Просмотр исходного кода

Merge pull request #2733 from porter-dev/login-revamp

Login revamp
jusrhee 3 лет назад
Родитель
Сommit
57e083a897

Разница между файлами не показана из-за своего большого размера
+ 722 - 1292
dashboard/package-lock.json


+ 5 - 6
dashboard/package.json

@@ -44,12 +44,12 @@
     "lodash": "^4.17.21",
     "markdown-to-jsx": "^7.0.1",
     "qs": "^6.9.4",
-    "random-words": "^1.1.1",
-    "react": "^16.13.1",
+    "random-word-slugs": "^0.1.6",
+    "react": "^16.14.0",
     "react-ace": "^9.1.3",
     "react-color": "^2.19.3",
     "react-datepicker": "^4.8.0",
-    "react-dom": "^16.13.1",
+    "react-dom": "^16.14.0",
     "react-error-boundary": "^3.1.3",
     "react-hot-toast": "^2.4.0",
     "react-infinite-scroll-component": "^6.1.0",
@@ -95,11 +95,10 @@
     "@types/material-ui": "^0.21.8",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
-    "@types/random-words": "^1.1.0",
-    "@types/react": "^16.14.14",
+    "@types/react": "^16.14.35",
     "@types/react-color": "^3.0.6",
     "@types/react-datepicker": "^4.4.2",
-    "@types/react-dom": "^16.9.8",
+    "@types/react-dom": "^16.9.18",
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
     "@types/react-router-dom": "^5.1.5",

BIN
dashboard/src/assets/blog.png


BIN
dashboard/src/assets/community.png


BIN
dashboard/src/assets/docs.png


+ 10 - 1
dashboard/src/components/porter/Button.tsx

@@ -10,6 +10,8 @@ type Props = {
   status?: string;
   loadingText?: string;
   successText?: string;
+  width?: string;
+  height?: string;
 };
 
 const Button: React.FC<Props> = ({
@@ -19,6 +21,8 @@ const Button: React.FC<Props> = ({
   status,
   loadingText,
   successText,
+  width,
+  height,
 }) => {
   const renderStatus = () => {
     switch(status) {
@@ -51,6 +55,8 @@ const Button: React.FC<Props> = ({
       <StyledButton
         disabled={disabled}
         onClick={() => !disabled && onClick()}
+        width={width}
+        height={height}
       >
         <Text>{children}</Text>
       </StyledButton>
@@ -114,8 +120,11 @@ const Text = styled.div`
 
 const StyledButton = styled.button<{
   disabled: boolean;
+  width: string;
+  height: string;
 }>`
-  height: 35px;
+  height: ${props => props.height || "35px"};
+  width: ${props => props.width || "auto"};
   font-size: 13px;
   cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
   padding: 15px;

+ 31 - 0
dashboard/src/components/porter/Container.tsx

@@ -0,0 +1,31 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+
+type Props = {
+  children: React.ReactNode;
+  row?: boolean;
+};
+
+const Container: React.FC<Props> = ({
+  children,
+  row,
+}) => {
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  return (
+    <StyledContainer
+      row={row}
+    >
+      {children}
+    </StyledContainer>
+  );
+};
+
+export default Container;
+
+const StyledContainer = styled.div<{
+  row: boolean;
+}>`
+  display: ${props => props.row ? "flex" : "block"};
+  align-items: center;
+`;

+ 70 - 11
dashboard/src/components/porter/Input.tsx

@@ -6,6 +6,11 @@ type Props = {
   width?: string;
   value: string;
   setValue: (value: string) => void;
+  label?: string;
+  height?: string;
+  type?: string;
+  error?: string;
+  children?: React.ReactNode;
 };
 
 const Input: React.FC<Props> = ({
@@ -13,32 +18,86 @@ const Input: React.FC<Props> = ({
   width,
   value,
   setValue,
+  label,
+  height,
+  type,
+  error,
+  children,
 }) => {
   return (
-    <StyledInput
-      value={value}
-      onChange={e => setValue(e.target.value)}
-      placeholder={placeholder}
-      width={width}
-    />
+    <Block width={width}>
+      {
+        label && (
+          <Label>{label}</Label>
+        )
+      }
+      <StyledInput
+        value={value}
+        onChange={e => setValue(e.target.value)}
+        placeholder={placeholder}
+        width={width}
+        height={height}
+        type={type || "text"}
+        hasError={(error && true) || (error === "")}
+      />
+      {
+        error && (
+          <Error>
+            <i className="material-icons">error</i>
+            {error}
+          </Error>
+        )
+      }
+      {children}
+    </Block>
   );
 };
 
 export default Input;
 
+const Block = styled.div<{
+  width: string;
+}>`
+  display: block;
+  position: relative;
+  width: ${props => props.width || "200px"};
+`;
+
+const Label = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+  margin-bottom: 10px;
+`;
+
+const Error = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #ff3b62;
+  margin-top: 10px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 5px;
+  }
+`;
+
+
 const StyledInput = styled.input<{
   width: string;
+  height: string;
+  hasError: boolean;
 }>`
-  height: 35px;
+  height: ${props => props.height || "35px"};
   padding: 5px 10px;
   width: ${props => props.width || "200px"};
-  color: white;
-  font-saize: 13px;
+  color: #ffffff;
+  font-size: 13px;
   outline: none;
   border-radius: 5px;
   background: #26292e;
-  border: 1px solid #494b4f;
+  border: 1px solid ${props => props.hasError ? "#ff3b62" : "#494b4f"};
   :hover {
-    border: 1px solid #7a7b80;
+    border: 1px solid ${props => props.hasError ? "#ff3b62" : "#7a7b80"};
   }
 `;

+ 37 - 0
dashboard/src/components/porter/Link.tsx

@@ -0,0 +1,37 @@
+import DynamicLink from "components/DynamicLink";
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+
+type Props = {
+  to?: string;
+  onClick?: () => void;
+  children: React.ReactNode;
+};
+
+const Link: React.FC<Props> = ({
+  to,
+  onClick,
+  children,
+}) => {
+  return (
+    <>
+      {to ? (
+        <StyledLink to={to}>{children}</StyledLink>
+      ) : (
+        <Div onClick={onClick}>{children}</Div>
+      )}
+    </>
+  );
+};
+
+export default Link;
+
+const Div = styled.span`
+  color: #8590ff;
+  cursor: pointer;
+`;
+
+const StyledLink = styled(DynamicLink)`
+  color: #8590ff;
+  cursor: pointer;
+`;

+ 10 - 1
dashboard/src/components/porter/Spacer.tsx

@@ -4,12 +4,14 @@ import styled from "styled-components";
 type Props = {
   height?: string;
   y?: number;
+  x?: number;
   inline?: boolean;
 };
 
 const Spacer: React.FC<Props> = ({
   height,
   y,
+  x,
   inline,
 }) => {
   const getCalcHeight = () => {
@@ -18,11 +20,18 @@ const Spacer: React.FC<Props> = ({
     }
     return null
   };
+
+  const getCalcWidth = () => {
+    if (x) {
+      return 15 * x + "px";
+    }
+    return "15px";
+  };
   
   return (
     <StyledSpacer
       height={height || getCalcHeight()}
-      width={inline && "15px"}
+      width={inline && getCalcWidth()}
     />
   );
 };

+ 1 - 1
dashboard/src/main/Main.tsx

@@ -119,7 +119,7 @@ export default class Main extends Component<PropsType, StateType> {
           <Route
             path="/"
             render={() => {
-              return <VerifyEmail handleLogout={this.handleLogOut} />;
+              return <VerifyEmail handleLogOut={this.handleLogOut} />;
             }}
           />
         </Switch>

+ 259 - 392
dashboard/src/main/auth/Login.tsx

@@ -1,90 +1,61 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
-import logo from "assets/logo.png";
+
 import github from "assets/github-icon.png";
+import logo from "assets/logo.png";
+import docs from "assets/docs.png";
+import blog from "assets/blog.png";
+import community from "assets/community.png";
 import GoogleIcon from "assets/GoogleIcon";
 
 import api from "shared/api";
 import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 
-type PropsType = {
-  authenticate: () => void;
-};
+import DynamicLink from "components/DynamicLink";
+import Heading from "components/form-components/Heading";
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Link from "components/porter/Link";
 
-type StateType = {
-  email: string;
-  password: string;
-  emailError: boolean;
-  credentialError: boolean;
-  hasBasic: boolean;
-  hasGithub: boolean;
-  hasGoogle: boolean;
-  hasResetPassword: boolean;
+type Props = {
+  authenticate: () => void;
 };
 
-export default class Login extends Component<PropsType, StateType> {
-  state = {
-    email: "",
-    password: "",
-    emailError: false,
-    credentialError: false,
-    hasBasic: true,
-    hasGithub: true,
-    hasGoogle: false,
-    hasResetPassword: true,
-  };
-
-  handleKeyDown = (e: any) => {
-    e.key === "Enter" ? this.handleLogin() : null;
-  };
-
-  componentDidMount() {
-    let urlParams = new URLSearchParams(window.location.search);
-    let emailFromCLI = urlParams.get("email");
-    emailFromCLI
-      ? this.setState({ email: emailFromCLI })
-      : document.addEventListener("keydown", this.handleKeyDown);
-
-    // get capabilities to case on github
-    api
-      .getMetadata("", {}, {})
-      .then((res) => {
-        this.setState({
-          hasBasic: res.data?.basic_login,
-          hasGithub: res.data?.github_login,
-          hasGoogle: res.data?.google_login,
-          hasResetPassword: res.data?.email,
-        });
-      })
-      .catch((err) => console.log(err));
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.handleKeyDown);
-  }
-
-  handleLogin = (): void => {
-    let { email, password } = this.state;
-    let { authenticate } = this.props;
-    let { setUser } = this.context;
+const getWindowDimensions = () => {
+  const { innerWidth: width, innerHeight: height } = window;
+  return { width, height };
+}
 
-    // Check for valid input
+const Login: React.FC<Props> = ({
+  authenticate,
+}) => {
+  const { setUser, setCurrentError } = useContext(Context);
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [emailError, setEmailError] = useState(false);
+  const [credentialError, setCredentialError] = useState(false);
+  const [hasBasic, setHasBasic] = useState(true);
+  const [hasGithub, setHasGithub] = useState(true);
+  const [hasGoogle, setHasGoogle] = useState(false);
+  const [hasResetPassword, setHasResetPassword] = useState(true);
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  const handleLogin = (): void => {
     if (!emailRegex.test(email)) {
-      this.setState({ emailError: true });
+      setEmailError(true);
+    } else if (password === "") {
+      setCredentialError(true);
     } else {
-      // Attempt user login
-      api
-        .logInUser(
-          "",
-          {
-            email: email,
-            password: password,
-          },
-          {}
-        )
+      api.logInUser(
+        "",
+        { email: email, password: password },
+        {}
+      )
         .then((res) => {
-          // TODO: case and set credential error
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
           } else {
@@ -92,396 +63,292 @@ export default class Login extends Component<PropsType, StateType> {
             authenticate();
           }
         })
-        .catch((err) => this.context.setCurrentError(err.response.data.error));
+        .catch((err) => setCurrentError(err.response.data.error));
     }
   };
 
-  renderEmailError = () => {
-    let { emailError } = this.state;
-    if (emailError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Please enter a valid email
-        </ErrorHelper>
-      );
-    }
+  const handleResize = () => {
+    setWindowDimensions(getWindowDimensions());
   };
 
-  renderCredentialError = () => {
-    let { credentialError } = this.state;
-    if (credentialError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Incorrect email or password
-        </ErrorHelper>
-      );
-    }
+  const handleKeyDown = (e: any) => {
+    if (e.key === "Enter") {
+      handleLogin();
+    };
   };
 
-  githubRedirect = () => {
+  // Manually re-register event listener on email/password change
+  useEffect(() => {
+    document.removeEventListener("keydown", handleKeyDown);
+    document.addEventListener("keydown", handleKeyDown);
+    return () => {
+      document.removeEventListener("keydown", handleKeyDown);
+    };
+  }, [email, password]);
+
+  useEffect(() => {
+
+    // Get capabilities to case on login methods
+    api.getMetadata("", {}, {})
+      .then((res) => {
+        setHasBasic(res.data?.basic_login);
+        setHasGithub(res.data?.github_login);
+        setHasGoogle(res.data?.google_login);
+        setHasResetPassword(res.data?.email);
+      })
+      .catch((err) => console.log(err));
+
+    const urlParams = new URLSearchParams(window.location.search);
+    const emailFromCLI = urlParams.get("email");
+    emailFromCLI && setEmail(emailFromCLI);
+
+    window.addEventListener('resize', handleResize);
+    return () => {
+      window.removeEventListener('resize', handleResize);
+    };
+  }, []);
+
+  const githubRedirect = () => {
     let redirectUrl = `/api/oauth/login/github`;
     window.location.href = redirectUrl;
   };
 
-  googleRedirect = () => {
+  const googleRedirect = () => {
     let redirectUrl = `/api/oauth/login/google`;
     window.location.href = redirectUrl;
   };
 
-  renderGithubSection = () => {
-    if (this.state.hasGithub) {
-      return (
-        <OAuthButton onClick={this.githubRedirect}>
-          <IconWrapper>
-            <Icon src={github} />
-            Log in with GitHub
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
-  };
-
-  renderGoogleSection = () => {
-    if (this.state.hasGoogle) {
-      return (
-        <OAuthButton onClick={this.googleRedirect}>
-          <IconWrapper>
-            <StyledGoogleIcon />
-            Log in with Google
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
-  };
-
-  renderBasicSection = () => {
-    if (this.state.hasBasic) {
-      let { email, password, credentialError, emailError } = this.state;
-
-      return (
-        <div>
-          <InputWrapper>
+  return (
+    <StyledLogin>
+      {windowDimensions.width > windowDimensions.height && (
+        <Wrapper>
+          <Logo src={logo} />
+          <Spacer y={2} />
+          <Jumbotron>
+            <Shiny>Welcome back to Porter</Shiny>
+          </Jumbotron>
+          <Spacer y={2} />
+          <LinkRow to="https://docs.porter.run" target="_blank">
+            <img src={docs} /> Read the Porter docs
+          </LinkRow>
+          <Spacer y={0.5} />
+          <LinkRow to="https://blog.porter.run" target="_blank">
+            <img src={blog} /> See what's new with Porter
+          </LinkRow>
+          <Spacer y={0.5} />
+          <LinkRow to="https://discord.com/invite/34n7NN7FJ7" target="_blank">
+            <img src={community} /> Join the community
+          </LinkRow>
+        </Wrapper>
+      )}
+      <Wrapper>
+        {windowDimensions.width <= windowDimensions.height && (
+          <Flex>
+            <Logo src={logo} />
+            <Spacer y={2} />
+          </Flex>
+        )}
+        <Heading isAtTop>
+          Log in to your Porter account
+        </Heading>
+        <Spacer y={1} />
+        {(hasGithub || hasGoogle) && (
+          <>
+            <Container row>
+              {hasGithub && (
+                <OAuthButton onClick={githubRedirect}>
+                  <Icon src={github} />
+                  Log in with GitHub
+                </OAuthButton>
+              )}
+              {hasGithub && hasGoogle && (
+                <Spacer inline x={2} />
+              )}
+              {hasGoogle && (
+                <OAuthButton onClick={googleRedirect}>
+                  <StyledGoogleIcon />
+                  Log in with Google
+                </OAuthButton>
+              )}
+            </Container>
+            {hasBasic && (
+              <OrWrapper>
+                <Line />
+                <Or>or</Or>
+              </OrWrapper>
+            )}
+          </>
+        )}
+        {hasBasic && (
+          <>
             <Input
               type="email"
               placeholder="Email"
+              label="Email"
               value={email}
-              onChange={(e: ChangeEvent<HTMLInputElement>) =>
-                this.setState({
-                  email: e.target.value,
-                  emailError: false,
-                  credentialError: false,
-                })
-              }
-              valid={!credentialError && !emailError}
+              setValue={(x) => {
+                setEmail(x);
+                setEmailError(false);
+                setCredentialError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={(emailError && "Please enter a valid email") || (credentialError && "")}
             />
-            {this.renderEmailError()}
-          </InputWrapper>
-          <InputWrapper>
+            <Spacer y={1} />
             <Input
               type="password"
               placeholder="Password"
+              label="Password"
               value={password}
-              onChange={(e: ChangeEvent<HTMLInputElement>) =>
-                this.setState({
-                  password: e.target.value,
-                  credentialError: false,
-                })
-              }
-              valid={!credentialError}
-            />
-            {this.renderCredentialError()}
-          </InputWrapper>
-          <Button onClick={this.handleLogin}>Continue</Button>
-        </div>
-      );
-    }
-  };
-
-  renderHelper() {
-    if (this.state.hasResetPassword) {
-      return (
-        <Helper>
-          <Link href="/register">Sign up</Link> |
-          <Link href="/password/reset">Forgot password?</Link>
-        </Helper>
-      );
-    }
-
-    return (
-      <Helper>
-        <Link href="/register">Sign up</Link>
-      </Helper>
-    );
-  }
-
-  render() {
-    return (
-      <StyledLogin>
-        <LoginPanel
-          hasBasic={this.state.hasBasic}
-          numOAuth={+this.state.hasGithub + +this.state.hasGoogle}
+              setValue={(x) => {
+                setPassword(x);
+                setCredentialError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={credentialError && ""}
+            >
+              {hasResetPassword && (
+                <ForgotPassword>
+                  <Link to="/password/reset">Forgot your password?</Link>
+                </ForgotPassword>
+              )}
+            </Input>
+            <Spacer height="30px" />
+            <Button onClick={handleLogin} width="100%" height="40px">
+              Continue
+            </Button>
+          </>
+        )}
+        <Spacer y={1} />
+        <Text 
+          size={13}
+          color="helper"
         >
-          <OverflowWrapper>
-            <GradientBg />
-          </OverflowWrapper>
-          <FormWrapper>
-            <Logo src={logo} />
-            <Prompt>Log in to Porter</Prompt>
-            {this.renderGithubSection()}
-            {this.renderGoogleSection()}
-            {(this.state.hasGithub || this.state.hasGoogle) &&
-            this.state.hasBasic ? (
-              <OrWrapper>
-                <Line />
-                <Or>or</Or>
-              </OrWrapper>
-            ) : null}
-            <DarkMatter />
-            {this.renderBasicSection()}
-            {this.renderHelper()}
-          </FormWrapper>
-        </LoginPanel>
-        <Footer>
-          © 2021 Porter Technologies Inc. •
-          <Link
-            href="https://docs.getporter.dev/docs/terms-of-service"
-            target="_blank"
-          >
-            Terms & Privacy
-          </Link>
-        </Footer>
-      </StyledLogin>
-    );
-  }
-}
+          Don't have an account? <Link to="/register">Sign up</Link>
+        </Text>
+      </Wrapper>
+    </StyledLogin>
+  );
+};
 
-Login.contextType = Context;
+export default Login;
 
-const Footer = styled.div`
+const ForgotPassword = styled.div`
   position: absolute;
-  bottom: 0;
-  left: 0;
-  margin-bottom: 30px;
-  width: 100vw;
-  text-align: center;
-  color: #aaaabb;
+  right: 0;
+  top: 0;
   font-size: 13px;
-  padding-right: 8px;
-  font: Work Sans, sans-serif;
-`;
-
-const DarkMatter = styled.div`
-  margin-top: -10px;
 `;
 
-const Or = styled.div`
-  position: absolute;
-  width: 30px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 15px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
+const Flex = styled.div`
   display: flex;
   align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
+  justify-content: center;
+  flex-direction: column;
 `;
 
-const IconWrapper = styled.div`
+const LinkRow = styled(DynamicLink)`
+  font-size: 14px;
   display: flex;
   align-items: center;
-  justify-content: center;
-  padding: 0 10px;
-  height: 100%;
-`;
+  width: 220px;
+  color: #aaaabb;
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: #4797ff;
+  }
 
-const Icon = styled.img`
-  height: 18px;
-  margin: 0 10px;
+  > img {
+    height: 18px;
+    margin-right: 10px;
+  }
+
+  :hover {
+    filter: brightness(2);
+  }
 `;
 
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
+const Shiny = styled.span`
+  background-image: linear-gradient(225deg, #fff, #7980ff);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
 `;
 
-const OAuthButton = styled.button`
-  width: 200px;
-  height: 30px;
-  border: 0;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 3px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
+const Jumbotron = styled.div`
+  font-size: 32px;
   font-weight: 500;
-  font-size: 13px;
-  margin: 10px 0;
-  overflow: hidden;
-  :hover {
-    background: #ffffffdd;
-  }
+  line-height: 1.5;
 `;
 
-const Link = styled.a`
-  margin-left: 5px;
-  color: #819bfd;
+const Logo = styled.img`
+  height: 24px;
+  user-select: none;
 `;
 
-const Helper = styled.div`
-  position: absolute;
-  bottom: 30px;
-  width: 100%;
-  text-align: center;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  color: #ffffff44;
+const StyledGoogleIcon = styled(GoogleIcon)`
+  width: 38px;
+  height: 38px;
 `;
 
-const OverflowWrapper = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
+const Line = styled.div`
+  height: 2px;
   width: 100%;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 10px;
+  background: #ffffff22;
+  margin: 35px 0px 30px;
 `;
 
-const ErrorHelper = styled.div`
+const Or = styled.div`
   position: absolute;
-  right: -185px;
-  top: 8px;
-  height: 30px;
-  width: 170px;
-  user-select: none;
-  background: #272731;
-  font-family: "Work Sans", sans-serif;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ff3b62;
-  border-radius: 3px;
-
-  > div {
-    background: #272731;
-    height: 15px;
-    width: 15px;
-    position: absolute;
-    left: -3px;
-    top: 7px;
-    transform: rotate(45deg);
-    z-index: -1;
-  }
-`;
-
-const Line = styled.div`
-  min-height: 3px;
-  width: 100px;
+  width: 50px;
+  text-align: center;
+  background: #111114;
   z-index: 999;
-  background: #ffffff22;
-  margin: 30px 0px 30px;
+  left: calc(50% - 25px);
+  margin-top: -1px;
 `;
 
-const Button = styled.button`
-  width: 200px;
-  min-height: 30px;
+const OrWrapper = styled.div`
   display: flex;
-  justify-content: center;
   align-items: center;
-  font-family: "Work Sans", sans-serif;
-  cursor: pointer;
-  margin-top: 9px;
-  border-radius: 2px;
-  border: 0;
-  background: #819bfd;
-  color: white;
-  font-weight: 500;
+  color: #ffffff44;
   font-size: 14px;
-`;
-
-const InputWrapper = styled.div`
   position: relative;
 `;
 
-const Input = styled.input`
-  width: 200px;
-  font-family: "Work Sans", sans-serif;
-  margin: 8px 0px;
-  height: 30px;
-  padding: 8px;
-  background: #ffffff12;
-  color: #ffffff;
-  border: ${(props: { valid?: boolean }) =>
-    props.valid ? "0" : "1px solid #ff3b62"};
-  border-radius: 2px;
-  font-size: 14px;
-`;
-
-const Prompt = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  font-size: 15px;
-  margin-bottom: 18px;
-`;
-
-const Logo = styled.img`
-  width: 110px;
-  margin-top: 55px;
-  margin-bottom: 40px;
-  user-select: none;
+const Icon = styled.img`
+  height: 18px;
+  margin: 14px;
 `;
 
-const FormWrapper = styled.div`
-  width: calc(100% - 8px);
-  height: calc(100% - 8px);
-  background: #111114;
-  z-index: 1;
-  border-radius: 10px;
+const OAuthButton = styled.div`
+  width: 100%;
+  height: 40px;
   display: flex;
-  flex-direction: column;
+  background: #ffffff;
   align-items: center;
-`;
-
-const GradientBg = styled.div`
-  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
-  width: 200%;
-  height: 200%;
-  position: absolute;
-  top: -50%;
-  left: -50%;
-  animation: flip 6s infinite linear;
-  @keyframes flip {
-    from {
-      transform: rotate(0deg);
-    }
-    to {
-      transform: rotate(360deg);
-    }
+  border-radius: 5px;
+  color: #000000;
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 13px;
+  :hover {
+    background: #ffffffdd;
   }
 `;
 
-const LoginPanel = styled.div`
-  width: 330px;
-  height: ${(props: { numOAuth: number; hasBasic: boolean }) =>
-    280 + +props.hasBasic * 150 + props.numOAuth * 50}px;
-  background: white;
+const Wrapper = styled.div`
+  width: 500px;
   margin-top: -20px;
-  border-radius: 10px;
-  display: flex;
-  justify-content: center;
   position: relative;
-  align-items: center;
+  padding: 25px;
+  border-radius: 5px;
+  font-size: 13px;
 `;
 
 const StyledLogin = styled.div`
@@ -494,4 +361,4 @@ const StyledLogin = styled.div`
   top: 0;
   left: 0;
   background: #111114;
-`;
+`;

+ 261 - 386
dashboard/src/main/auth/Register.tsx

@@ -1,97 +1,67 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
-import logo from "assets/logo.png";
+
 import github from "assets/github-icon.png";
+import logo from "assets/logo.png";
 import GoogleIcon from "assets/GoogleIcon";
 
 import api from "shared/api";
 import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 
-type PropsType = {
-  authenticate: () => void;
-};
+import Heading from "components/form-components/Heading";
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Link from "components/porter/Link";
 
-type StateType = {
-  email: string;
-  password: string;
-  confirmPassword: string;
-  emailError: boolean;
-  confirmPasswordError: boolean;
-  hasGithub: boolean;
-  hasGoogle: boolean;
-  hasBasic: boolean;
+type Props = {
+  authenticate: () => void;
 };
 
-export default class Register extends Component<PropsType, StateType> {
-  state = {
-    email: "",
-    password: "",
-    confirmPassword: "",
-    emailError: false,
-    confirmPasswordError: false,
-    hasBasic: true,
-    hasGithub: true,
-    hasGoogle: false,
-  };
-
-  handleKeyDown = (e: any) => {
-    e.key === "Enter" ? this.handleRegister() : null;
-  };
-
-  componentDidMount() {
-    document.addEventListener("keydown", this.handleKeyDown);
-
-    // get capabilities to case on github
-    api
-      .getMetadata("", {}, {})
-      .then((res) => {
-        this.setState({
-          hasGithub: res.data?.github_login,
-          hasGoogle: res.data?.google_login,
-          hasBasic: res.data?.basic_login,
-        });
-      })
-      .catch((err) => console.log(err));
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.handleKeyDown);
-  }
-
-  githubRedirect = () => {
-    let redirectUrl = `/api/oauth/login/github`;
-    window.location.href = redirectUrl;
-  };
-
-  googleRedirect = () => {
-    let redirectUrl = `/api/oauth/login/google`;
-    window.location.href = redirectUrl;
-  };
-
-  handleRegister = (): void => {
-    let { email, password, confirmPassword } = this.state;
-    let { authenticate } = this.props;
-    let { setCurrentError, setUser } = this.context;
+const getWindowDimensions = () => {
+  const { innerWidth: width, innerHeight: height } = window;
+  return { width, height };
+}
 
+const Register: React.FC<Props> = ({
+  authenticate,
+}) => {
+  const { setUser, setCurrentError } = useContext(Context);
+  const [firstName, setFirstName] = useState("");
+  const [firstNameError, setFirstNameError] = useState(false);
+  const [lastName, setLastName] = useState("");
+  const [lastNameError, setLastNameError] = useState(false);
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [emailError, setEmailError] = useState(false);
+  const [hasBasic, setHasBasic] = useState(true);
+  const [hasGithub, setHasGithub] = useState(true);
+  const [hasGoogle, setHasGoogle] = useState(false);
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  const handleRegister = (): void => {
     if (!emailRegex.test(email)) {
-      this.setState({ emailError: true });
+      setEmailError(true);
     }
 
-    if (confirmPassword !== password) {
-      this.setState({ confirmPasswordError: true });
+    if (firstName === "") {
+      setFirstNameError(true);
+    }
+
+    if (lastName === "") {
+      setLastNameError(true);
     }
 
     // Check for valid input
-    if (emailRegex.test(email) && confirmPassword === password) {
+    if (emailRegex.test(email) && firstName !== "" && lastName !== "") {
       // Attempt user registration
       api
         .registerUser(
           "",
-          {
-            email: email,
-            password: password,
-          },
+          { email: email, password: password },
           {}
         )
         .then((res: any) => {
@@ -106,183 +76,245 @@ export default class Register extends Component<PropsType, StateType> {
     }
   };
 
-  renderEmailError = () => {
-    let { emailError } = this.state;
-    if (emailError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Please enter a valid email
-        </ErrorHelper>
-      );
-    }
+  const handleResize = () => {
+    setWindowDimensions(getWindowDimensions());
   };
 
-  renderConfirmPasswordError = () => {
-    let { confirmPasswordError } = this.state;
-    if (confirmPasswordError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Passwords do not match
-        </ErrorHelper>
-      );
-    }
+  const handleKeyDown = (e: any) => {
+    if (e.key === "Enter") {
+      handleRegister();
+    };
   };
 
-  renderGithubSection = () => {
-    if (this.state.hasGithub) {
-      return (
-        <OAuthButton onClick={this.githubRedirect}>
-          <IconWrapper>
-            <Icon src={github} />
-            Sign up with GitHub
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
+  // Manually re-register event listener on email/password change
+  useEffect(() => {
+    document.removeEventListener("keydown", handleKeyDown);
+    document.addEventListener("keydown", handleKeyDown);
+    return () => {
+      document.removeEventListener("keydown", handleKeyDown);
+    };
+  }, [email, password, firstName, lastName]);
+
+  useEffect(() => {
+    window.addEventListener('resize', handleResize);
+    return () => window.removeEventListener('resize', handleResize);
+  }, []);
+
+  const githubRedirect = () => {
+    let redirectUrl = `/api/oauth/login/github`;
+    window.location.href = redirectUrl;
   };
 
-  renderGoogleSection = () => {
-    if (this.state.hasGoogle) {
-      return (
-        <OAuthButton onClick={this.googleRedirect}>
-          <IconWrapper>
-            <StyledGoogleIcon />
-            Sign up with Google
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
+  const googleRedirect = () => {
+    let redirectUrl = `/api/oauth/login/google`;
+    window.location.href = redirectUrl;
   };
 
-  renderBasicSection = () => {
-    let {
-      email,
-      password,
-      confirmPassword,
-      emailError,
-      confirmPasswordError,
-    } = this.state;
-
-    if (this.state.hasBasic) {
-      return (
-        <div>
-          <InputWrapper>
+  return (
+    <StyledRegister>
+      {windowDimensions.width > windowDimensions.height && (
+        <Wrapper>
+          <Logo src={logo} />
+          <Spacer y={2} />
+          <Jumbotron>
+            Deploy and scale <Shiny>effortlessly</Shiny> with Porter
+          </Jumbotron>
+          <Spacer y={2} />
+          <CheckRow>
+            <i className="material-icons">done</i> Generous free tier for small teams
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Bring your own cloud (and cloud credits)
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Fully automated setup and deployment
+          </CheckRow>
+        </Wrapper>
+      )}
+      <Wrapper>
+        {windowDimensions.width <= windowDimensions.height && (
+          <Flex>
+            <Logo src={logo} />
+            <Spacer y={2} />
+          </Flex>
+        )}
+        <Heading isAtTop>
+          Create your Porter account
+        </Heading>
+        <Spacer y={1} />
+        {(hasGithub || hasGoogle) && (
+          <>
+            <Container row>
+              {hasGithub && (
+                <OAuthButton onClick={githubRedirect}>
+                  <Icon src={github} />
+                  Sign up with GitHub
+                </OAuthButton>
+              )}
+              {hasGithub && hasGoogle && (
+                <Spacer inline x={2} />
+              )}
+              {hasGoogle && (
+                <OAuthButton onClick={googleRedirect}>
+                  <StyledGoogleIcon />
+                  Sign up with Google
+                </OAuthButton>
+              )}
+            </Container>
+            {hasBasic && (
+              <OrWrapper>
+                <Line />
+                <Or>or</Or>
+              </OrWrapper>
+            )}
+          </>
+        )}
+        {hasBasic && (
+          <>
+            <Container row>
+              <RowWrapper>
+                <Input
+                  placeholder="First name"
+                  label="First name"
+                  value={firstName}
+                  setValue={(x) => {
+                    setFirstName(x);
+                    setFirstNameError(false);
+                  }}
+                  width="100%"
+                  height="40px"
+                  error={(firstNameError && "First name cannot be blank")}
+                />
+                {!firstNameError && lastNameError && (
+                  <Spacer height="27px" />
+                )}
+              </RowWrapper>
+              <Spacer inline x={2} />
+              <RowWrapper>
+                <Input
+                  placeholder="Last name"
+                  label="Last name"
+                  value={lastName}
+                  setValue={(x) => {
+                    setLastName(x);
+                    setLastNameError(false);
+                  }}
+                  width="100%"
+                  height="40px"
+                  error={(lastNameError && "Last name cannot be blank")}
+                />
+                {!lastNameError && firstNameError && (
+                  <Spacer height="27px" />
+                )}
+              </RowWrapper>
+            </Container>
+            <Spacer y={1} />
             <Input
               type="email"
               placeholder="Email"
+              label="Email"
               value={email}
-              onChange={(e: ChangeEvent<HTMLInputElement>) =>
-                this.setState({ email: e.target.value, emailError: false })
-              }
-              valid={!emailError}
+              setValue={(x) => {
+                setEmail(x);
+                setEmailError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={(emailError && "Please enter a valid email")}
             />
-            {this.renderEmailError()}
-          </InputWrapper>
-          <Input
-            type="password"
-            placeholder="Password"
-            value={password}
-            onChange={(e: ChangeEvent<HTMLInputElement>) =>
-              this.setState({
-                password: e.target.value,
-                confirmPasswordError: false,
-              })
-            }
-            valid={true}
-          />
-          <InputWrapper>
+            <Spacer y={1} />
             <Input
+              placeholder="Password"
+              label="Password"
+              value={password}
+              setValue={setPassword}
+              width="100%"
+              height="40px"
               type="password"
-              placeholder="Confirm Password"
-              value={confirmPassword}
-              onChange={(e: ChangeEvent<HTMLInputElement>) =>
-                this.setState({
-                  confirmPassword: e.target.value,
-                  confirmPasswordError: false,
-                })
-              }
-              valid={!confirmPasswordError}
             />
-            {this.renderConfirmPasswordError()}
-          </InputWrapper>
-          <Button onClick={this.handleRegister}>Continue</Button>
-        </div>
-      );
-    }
-  };
-
-  render() {
-    return (
-      <StyledRegister>
-        <LoginPanel
-          hasBasic={this.state.hasBasic}
-          numOAuth={+this.state.hasGithub + +this.state.hasGoogle}
+            <Spacer height="30px" />
+            <Button onClick={handleRegister} width="100%" height="40px">
+              Continue
+            </Button>
+          </>
+        )}
+        <Spacer y={1} />
+        <Text 
+          size={13}
+          color="helper"
         >
-          <OverflowWrapper>
-            <GradientBg />
-          </OverflowWrapper>
-          <FormWrapper>
-            <Logo src={logo} />
-            <Prompt>Sign up for Porter</Prompt>
-            {this.renderGithubSection()}
-            {this.renderGoogleSection()}
-            {(this.state.hasGithub || this.state.hasGoogle) &&
-            this.state.hasBasic ? (
-              <OrWrapper>
-                <Line />
-                <Or>or</Or>
-              </OrWrapper>
-            ) : null}
-            <DarkMatter />
-            {this.renderBasicSection()}
-            <Helper>
-              Have an account?
-              <Link href="/login">Sign in</Link>
-            </Helper>
-          </FormWrapper>
-        </LoginPanel>
-        <Footer>
-          © 2021 Porter Technologies Inc. •
-          <Link
-            href="https://docs.getporter.dev/docs/terms-of-service"
-            target="_blank"
-          >
-            Terms & Privacy
-          </Link>
-        </Footer>
-      </StyledRegister>
-    );
-  }
-}
+          Already have an account? <Link to="/login">Log in</Link>
+        </Text>
+      </Wrapper>
+    </StyledRegister>
+  );
+};
 
-Register.contextType = Context;
+export default Register;
 
-const Footer = styled.div`
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  margin-bottom: 30px;
-  width: 100vw;
-  text-align: center;
+const RowWrapper = styled.div`
+  width: 100%;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+`;
+
+const CheckRow = styled.div`
+  font-size: 14px;
+  display: flex;
+  align-items: center;
   color: #aaaabb;
-  font-size: 13px;
-  padding-right: 8px;
-  font: Work Sans, sans-serif;
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: #4797ff;
+  }
+`;
+
+const Shiny = styled.span`
+  background-image: linear-gradient(225deg, #fff, #7980ff);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+`;
+
+const Jumbotron = styled.div`
+  font-size: 32px;
+  font-weight: 500;
+  line-height: 1.5;
+`;
+
+const Logo = styled.img`
+  height: 24px;
+  user-select: none;
+`;
+
+const StyledGoogleIcon = styled(GoogleIcon)`
+  width: 38px;
+  height: 38px;
 `;
 
-const DarkMatter = styled.div`
-  margin-top: -10px;
+const Line = styled.div`
+  height: 2px;
+  width: 100%;
+  background: #ffffff22;
+  margin: 35px 0px 30px;
 `;
 
 const Or = styled.div`
   position: absolute;
-  width: 30px;
+  width: 50px;
   text-align: center;
   background: #111114;
   z-index: 999;
-  left: calc(50% - 15px);
+  left: calc(50% - 25px);
   margin-top: -1px;
 `;
 
@@ -294,192 +326,35 @@ const OrWrapper = styled.div`
   position: relative;
 `;
 
-const IconWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 10px;
-  height: 100%;
-`;
-
 const Icon = styled.img`
   height: 18px;
   margin: 14px;
 `;
 
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
-`;
-
 const OAuthButton = styled.div`
-  width: 200px;
-  height: 30px;
+  width: 100%;
+  height: 40px;
   display: flex;
   background: #ffffff;
   align-items: center;
-  border-radius: 3px;
+  border-radius: 5px;
   color: #000000;
   cursor: pointer;
   user-select: none;
   font-weight: 500;
   font-size: 13px;
-  margin: 10px 0;
-  overflow: hidden;
   :hover {
     background: #ffffffdd;
   }
 `;
 
-const Link = styled.a`
-  margin-left: 5px;
-  color: #819bfd;
-`;
-
-const Helper = styled.div`
-  position: absolute;
-  bottom: 30px;
-  width: 100%;
-  text-align: center;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  color: #ffffff44;
-`;
-
-const OverflowWrapper = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 10px;
-`;
-
-const InputWrapper = styled.div`
-  position: relative;
-`;
-
-const ErrorHelper = styled.div`
-  position: absolute;
-  right: -185px;
-  top: 8px;
-  height: 30px;
-  width: 170px;
-  user-select: none;
-  background: #272731;
-  font-family: "Work Sans", sans-serif;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ff3b62;
-  border-radius: 3px;
-
-  > div {
-    background: #272731;
-    height: 15px;
-    width: 15px;
-    position: absolute;
-    left: -3px;
-    top: 7px;
-    transform: rotate(45deg);
-    z-index: -1;
-  }
-`;
-
-const Line = styled.div`
-  height: 3px;
-  width: 100px;
-  background: #ffffff22;
-  margin: 35px 0px 30px;
-`;
-
-const Button = styled.button`
-  width: 200px;
-  height: 30px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  cursor: pointer;
-  margin-top: 9px;
-  border-radius: 2px;
-  border: 0;
-  background: #819bfd;
-  color: white;
-  font-weight: 500;
-  font-size: 14px;
-`;
-
-const Input = styled.input`
-  width: 200px;
-  font-family: "Work Sans", sans-serif;
-  margin: 8px 0px;
-  height: 30px;
-  padding: 8px;
-  background: #ffffff12;
-  color: #ffffff;
-  border: ${(props: { valid?: boolean }) =>
-    props.valid ? "0" : "1px solid #ff3b62"};
-  border-radius: 2px;
-  font-size: 14px;
-`;
-
-const Prompt = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  font-size: 15px;
-  margin-bottom: 18px;
-`;
-
-const Logo = styled.img`
-  width: 110px;
-  margin-top: 45px;
-  margin-bottom: 30px;
-  user-select: none;
-`;
-
-const FormWrapper = styled.div`
-  width: calc(100% - 8px);
-  height: calc(100% - 8px);
-  background: #111114;
-  z-index: 1;
-  border-radius: 10px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-`;
-
-const GradientBg = styled.div`
-  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
-  width: 200%;
-  height: 200%;
-  position: absolute;
-  top: -50%;
-  left: -50%;
-  animation: flip 6s infinite linear;
-  @keyframes flip {
-    from {
-      transform: rotate(0deg);
-    }
-    to {
-      transform: rotate(360deg);
-    }
-  }
-`;
-
-const LoginPanel = styled.div`
-  width: 330px;
-  height: ${(props: { numOAuth: number; hasBasic: boolean }) =>
-    270 + +props.hasBasic * 180 + props.numOAuth * 50}px;
-  background: white;
+const Wrapper = styled.div`
+  width: 500px;
   margin-top: -20px;
-  border-radius: 10px;
-  display: flex;
-  justify-content: center;
   position: relative;
-  align-items: center;
+  padding: 25px;
+  border-radius: 5px;
+  font-size: 13px;
 `;
 
 const StyledRegister = styled.div`
@@ -492,4 +367,4 @@ const StyledRegister = styled.div`
   top: 0;
   left: 0;
   background: #111114;
-`;
+`;

+ 179 - 279
dashboard/src/main/auth/VerifyEmail.tsx

@@ -1,148 +1,208 @@
-import React, { Component } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
+
+import github from "assets/github-icon.png";
 import logo from "assets/logo.png";
+import GoogleIcon from "assets/GoogleIcon";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
 
-type PropsType = {
-  handleLogout: () => void;
-};
+import Heading from "components/form-components/Heading";
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Link from "components/porter/Link";
 
-type StateType = {
-  submitted: boolean;
+type Props = {
+  handleLogOut: () => void;
 };
 
-export default class VerifyEmail extends Component<PropsType, StateType> {
-  state = {
-    submitted: false,
+const getWindowDimensions = () => {
+  const { innerWidth: width, innerHeight: height } = window;
+  return { width, height };
+}
+
+const Register: React.FC<Props> = ({
+  handleLogOut,
+}) => {
+  const { user, setCurrentError } = useContext(Context);
+  const [submitted, setSubmitted] = useState(false);
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  const handleResize = () => {
+    setWindowDimensions(getWindowDimensions());
   };
 
-  handleSendEmail = (): void => {
-    api
-      .createEmailVerification("", {}, {})
+  useEffect(() => {
+    window.addEventListener('resize', handleResize);
+    return () => window.removeEventListener('resize', handleResize);
+  }, []);
+
+  const handleSendEmail = (): void => {
+    api.createEmailVerification("", {}, {})
       .then((res) => {
-        this.setState({ submitted: true });
+        setSubmitted(true);
       })
-      .catch((err) => this.context.setCurrentError(err.response.data.error));
+      .catch((err) => setCurrentError(err.response.data.error));
   };
 
-  render() {
-    let { submitted } = this.state;
-
-    let formSection = (
-      <div>
-        <InputWrapper>
-          <StatusText>A verification email should have been sent to</StatusText>
-          <Email>{this.context.user?.email}</Email>
-        </InputWrapper>
-        <StatusText>Didn't get it?</StatusText>
-        <Button onClick={this.handleSendEmail}>
-          Resend Verification Email
-        </Button>
-      </div>
-    );
-
-    if (submitted) {
-      formSection = (
-        <>
-          <Buffer />
-          <StatusText lessPadding={true}>
-            A verification email was sent to{" "}
-            <White>{this.context.user?.email}</White>
-          </StatusText>
-          <StatusText lessPadding={true}>
-            Check your inbox for a verification email. Don't forget to check
-            your spam folder
-          </StatusText>
-          <StatusText lessPadding={true}>
-            Need help?
-            <Link href="mailto:contact@getporter.dev">Contact us</Link>
-          </StatusText>
-        </>
-      );
-    }
-
-    return (
-      <StyledLogin>
-        <LoginPanel>
-          <OverflowWrapper>
-            <GradientBg />
-          </OverflowWrapper>
-          <FormWrapper>
+  return (
+    <StyledRegister>
+      {windowDimensions.width > windowDimensions.height && (
+        <Wrapper>
+          <Logo src={logo} />
+          <Spacer y={2} />
+          <Jumbotron>
+            Deploy and scale <Shiny>effortlessly</Shiny> with Porter
+          </Jumbotron>
+          <Spacer y={2} />
+          <CheckRow>
+            <i className="material-icons">done</i> Generous free tier for small teams
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Bring your own cloud (and cloud credits)
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Fully automated setup and deployment
+          </CheckRow>
+        </Wrapper>
+      )}
+      <Wrapper>
+        {windowDimensions.width <= windowDimensions.height && (
+          <Flex>
             <Logo src={logo} />
-            <Prompt>Verify Your Email</Prompt>
-            <DarkMatter />
-            {formSection}
-            <Helper>
-              Want to use a different email?
-              <Link onClick={this.props.handleLogout}>Log out</Link>
-            </Helper>
-          </FormWrapper>
-        </LoginPanel>
-
-        <Footer>
-          © 2021 Porter Technologies Inc. •
-          <Link
-            href="https://docs.getporter.dev/docs/terms-of-service"
-            target="_blank"
-          >
-            Terms & Privacy
-          </Link>
-        </Footer>
-      </StyledLogin>
-    );
-  }
-}
+            <Spacer y={2} />
+          </Flex>
+        )}
+        <Heading isAtTop>
+          Verify your email
+        </Heading>
+        <Spacer y={1} />
+        {submitted ? (
+          <>
+            <Text color="helper" size={13}>
+              A new verification email was sent to:
+            </Text>
+            <Spacer y={1} />
+            <Email>{user?.email}</Email>
+            <Spacer y={1} />
+            <Text color="helper" size={13}>
+              Don't forget to check your spam folder.
+            </Text>
+            <Spacer y={1} />
+            <Text color="helper" size={13}>
+              If you still need help, please contact support@porter.run.
+            </Text>
+          </>
+        ) : (
+          <>
+            <Text color="helper" size={13}>
+              We've sent a verification link to the following email address:
+            </Text>
+            <Spacer y={1} />
+            <Email>{user?.email}</Email>
+            <Spacer y={1} />
+            <Text color="helper" size={13}>
+              Please click the link in your inbox to verify your email.
+            </Text>
+            <Spacer y={1} />
+            <Text color="helper" size={13}>
+              Didn't receive anything?
+            </Text>
+            <Spacer height="30px" />
+            <Button onClick={handleSendEmail} width="100%" height="40px">
+              Resend verification email
+            </Button>
+          </>
+        )}
+        <Spacer y={1} />
+        <Text 
+          size={13}
+          color="helper"
+        >
+          Want to use a different email? <Link onClick={handleLogOut}>Log out</Link>
+        </Text>
+      </Wrapper>
+    </StyledRegister>
+  );
+};
 
-VerifyEmail.contextType = Context;
+export default Register;
 
-const Buffer = styled.div`
+const Email = styled.div`
   width: 100%;
-  height: 20px;
+  height: 40px;
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  padding: 15px;
 `;
 
-const White = styled.div`
-  color: white;
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
 `;
 
-const Email = styled.div`
-  background: #ffffff11;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
+const CheckRow = styled.div`
   font-size: 14px;
-  color: #aaaabb;
-  height: 30px;
-  margin: 0 60px;
   display: flex;
   align-items: center;
-  justify-content: center;
+  color: #aaaabb;
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: #4797ff;
+  }
 `;
 
-const Footer = styled.div`
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  margin-bottom: 30px;
-  width: 100vw;
-  text-align: center;
-  color: #aaaabb;
-  font-size: 13px;
-  padding-right: 8px;
-  font: Work Sans, sans-serif;
+const Shiny = styled.span`
+  background-image: linear-gradient(225deg, #fff, #7980ff);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
 `;
 
-const DarkMatter = styled.div`
-  margin-top: -20px;
+const Jumbotron = styled.div`
+  font-size: 32px;
+  font-weight: 500;
+  line-height: 1.5;
+`;
+
+const Logo = styled.img`
+  height: 24px;
+  user-select: none;
+`;
+
+const StyledGoogleIcon = styled(GoogleIcon)`
+  width: 38px;
+  height: 38px;
+`;
+
+const Line = styled.div`
+  height: 2px;
+  width: 100%;
+  background: #ffffff22;
+  margin: 35px 0px 30px;
 `;
 
 const Or = styled.div`
   position: absolute;
-  width: 30px;
+  width: 50px;
   text-align: center;
   background: #111114;
   z-index: 999;
-  left: calc(50% - 15px);
+  left: calc(50% - 25px);
   margin-top: -1px;
 `;
 
@@ -154,26 +214,18 @@ const OrWrapper = styled.div`
   position: relative;
 `;
 
-const IconWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 10px;
-  height: 100%;
-`;
-
 const Icon = styled.img`
   height: 18px;
-  margin-right: 20px;
+  margin: 14px;
 `;
 
 const OAuthButton = styled.div`
-  width: 200px;
-  height: 30px;
+  width: 100%;
+  height: 40px;
   display: flex;
   background: #ffffff;
   align-items: center;
-  border-radius: 3px;
+  border-radius: 5px;
   color: #000000;
   cursor: pointer;
   user-select: none;
@@ -184,168 +236,16 @@ const OAuthButton = styled.div`
   }
 `;
 
-const Link = styled.a`
-  margin-left: 5px;
-  color: #819bfd;
-  cursor: pointer;
-`;
-
-const Helper = styled.div`
-  position: absolute;
-  bottom: 30px;
-  width: 100%;
-  text-align: center;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  color: #ffffff44;
-`;
-
-const OverflowWrapper = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 10px;
-`;
-
-const ErrorHelper = styled.div`
-  position: absolute;
-  right: -185px;
-  top: 8px;
-  height: 30px;
-  width: 170px;
-  user-select: none;
-  background: #272731;
-  font-family: "Work Sans", sans-serif;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ff3b62;
-  border-radius: 3px;
-
-  > div {
-    background: #272731;
-    height: 15px;
-    width: 15px;
-    position: absolute;
-    left: -3px;
-    top: 7px;
-    transform: rotate(45deg);
-    z-index: -1;
-  }
-`;
-
-const Line = styled.div`
-  min-height: 3px;
-  width: 100px;
-  z-index: 999;
-  background: #ffffff22;
-  margin: 30px 0px 30px;
-`;
-
-const Button = styled.button`
-  width: 200px;
-  min-height: 30px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  cursor: pointer;
-  margin: 9px auto;
-  border-radius: 2px;
-  border: 0;
-  background: #819bfd;
-  color: white;
-  font-weight: 500;
-  font-size: 14px;
-`;
-
-const InputWrapper = styled.div`
-  position: relative;
-`;
-
-const Input = styled.input`
-  width: 200px;
-  font-family: "Work Sans", sans-serif;
-  margin: 8px 0px;
-  height: 30px;
-  padding: 8px;
-  background: #ffffff12;
-  color: #ffffff;
-  border: ${(props: { valid?: boolean }) =>
-    props.valid ? "0" : "1px solid #ff3b62"};
-  border-radius: 2px;
-  font-size: 14px;
-`;
-
-const Prompt = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  font-size: 15px;
-  margin-bottom: 18px;
-`;
-
-const Logo = styled.img`
-  width: 140px;
-  margin-top: 50px;
-  margin-bottom: 60px;
-  user-select: none;
-`;
-
-const StatusText = styled.div<{ lessPadding?: boolean }>`
-  padding: ${(props) => (props.lessPadding ? "10px" : "18px")} 40px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  line-height: 160%;
-  color: #aaaabb;
-  text-align: center;
-`;
-
-const FormWrapper = styled.div`
-  width: calc(100% - 8px);
-  height: calc(100% - 8px);
-  background: #111114;
-  z-index: 1;
-  border-radius: 10px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-`;
-
-const GradientBg = styled.div`
-  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
-  width: 180%;
-  height: 180%;
-  position: absolute;
-  top: -40%;
-  left: -40%;
-  animation: flip 6s infinite linear;
-  @keyframes flip {
-    from {
-      transform: rotate(0deg);
-    }
-    to {
-      transform: rotate(360deg);
-    }
-  }
-`;
-
-const LoginPanel = styled.div`
-  width: 330px;
-  height: 470px;
-  background: white;
+const Wrapper = styled.div`
+  width: 500px;
   margin-top: -20px;
-  border-radius: 10px;
-  display: flex;
-  justify-content: center;
   position: relative;
-  align-items: center;
+  padding: 25px;
+  border-radius: 5px;
+  font-size: 13px;
 `;
 
-const StyledLogin = styled.div`
+const StyledRegister = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
@@ -355,4 +255,4 @@ const StyledLogin = styled.div`
   top: 0;
   left: 0;
   background: #111114;
-`;
+`;

+ 2 - 2
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -1,7 +1,7 @@
 import React, { useContext, useState } from "react";
 import styled from "styled-components";
 import _ from "lodash";
-import randomWords from "random-words";
+import { generateSlug } from "random-word-slugs";
 import { RouteComponentProps, withRouter } from "react-router";
 
 import api from "shared/api";
@@ -66,7 +66,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
   const [buildConfig, setBuildConfig] = useState();
 
   const generateRandomName = () => {
-    const randomTemplateName = randomWords({ exactly: 3, join: "-" });
+    const randomTemplateName = generateSlug();
     return randomTemplateName;
   };
 

+ 1 - 1
dashboard/src/shared/error_handling/logger.ts

@@ -33,7 +33,7 @@ function buildLogger(scope: string = "global") {
       if (typeof currentSeverity === "string") {
         acc[currentSeverity] = logFunctionBuilder(
           scope,
-          Sentry.Severity.fromString(currentSeverity)
+          Sentry.Severity.Info
         );
       }
 

Некоторые файлы не были показаны из-за большого количества измененных файлов