Prechádzať zdrojové kódy

revamped login/register layout, reimplemented login

Justin Rhee 3 rokov pred
rodič
commit
caae294419

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 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;
+`;

+ 69 - 10
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;
+  color: #ffffff;
   font-saize: 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"};
   }
 `;

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

@@ -0,0 +1,26 @@
+import DynamicLink from "components/DynamicLink";
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+
+type Props = {
+  to: string;
+  children: React.ReactNode;
+};
+
+const Link: React.FC<Props> = ({
+  to,
+  children,
+}) => {
+  return (
+    <StyledLink to={to}>
+      {children}
+    </StyledLink>
+  );
+};
+
+export default Link;
+
+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()}
     />
   );
 };

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

@@ -1,90 +1,59 @@
-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 {
-      // 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 +61,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 && "Incorrect email or password"}
+            >
+              {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 +359,4 @@ const StyledLogin = styled.div`
   top: 0;
   left: 0;
   background: #111114;
-`;
+`;

+ 497 - 0
dashboard/src/main/auth/OldLogin.tsx

@@ -0,0 +1,497 @@
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
+import logo from "assets/logo.png";
+import github from "assets/github-icon.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;
+};
+
+type StateType = {
+  email: string;
+  password: string;
+  emailError: boolean;
+  credentialError: boolean;
+  hasBasic: boolean;
+  hasGithub: boolean;
+  hasGoogle: boolean;
+  hasResetPassword: boolean;
+};
+
+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;
+
+    // Check for valid input
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    } else {
+      // Attempt user login
+      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 {
+            setUser(res?.data?.id, res?.data?.email);
+            authenticate();
+          }
+        })
+        .catch((err) => this.context.setCurrentError(err.response.data.error));
+    }
+  };
+
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Please enter a valid email
+        </ErrorHelper>
+      );
+    }
+  };
+
+  renderCredentialError = () => {
+    let { credentialError } = this.state;
+    if (credentialError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Incorrect email or password
+        </ErrorHelper>
+      );
+    }
+  };
+
+  githubRedirect = () => {
+    let redirectUrl = `/api/oauth/login/github`;
+    window.location.href = redirectUrl;
+  };
+
+  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>
+            <Input
+              type="email"
+              placeholder="Email"
+              value={email}
+              onChange={(e: ChangeEvent<HTMLInputElement>) =>
+                this.setState({
+                  email: e.target.value,
+                  emailError: false,
+                  credentialError: false,
+                })
+              }
+              valid={!credentialError && !emailError}
+            />
+            {this.renderEmailError()}
+          </InputWrapper>
+          <InputWrapper>
+            <Input
+              type="password"
+              placeholder="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}
+        >
+          <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>
+    );
+  }
+}
+
+Login.contextType = Context;
+
+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 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`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  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: 0 10px;
+`;
+
+const StyledGoogleIcon = styled(GoogleIcon)`
+  width: 38px;
+  height: 38px;
+`;
+
+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;
+  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 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-top: 9px;
+  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: 110px;
+  margin-top: 55px;
+  margin-bottom: 40px;
+  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 }) =>
+    280 + +props.hasBasic * 150 + props.numOAuth * 50}px;
+  background: white;
+  margin-top: -20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  position: relative;
+  align-items: center;
+`;
+
+const StyledLogin = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 495 - 0
dashboard/src/main/auth/OldRegister.tsx

@@ -0,0 +1,495 @@
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
+import logo from "assets/logo.png";
+import github from "assets/github-icon.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;
+};
+
+type StateType = {
+  email: string;
+  password: string;
+  confirmPassword: string;
+  emailError: boolean;
+  confirmPasswordError: boolean;
+  hasGithub: boolean;
+  hasGoogle: boolean;
+  hasBasic: boolean;
+};
+
+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;
+
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    }
+
+    if (confirmPassword !== password) {
+      this.setState({ confirmPasswordError: true });
+    }
+
+    // Check for valid input
+    if (emailRegex.test(email) && confirmPassword === password) {
+      // Attempt user registration
+      api
+        .registerUser(
+          "",
+          {
+            email: email,
+            password: password,
+          },
+          {}
+        )
+        .then((res: any) => {
+          if (res?.data?.redirect) {
+            window.location.href = res.data.redirect;
+          } else {
+            setUser(res?.data?.id, res?.data?.email);
+            authenticate();
+          }
+        })
+        .catch((err) => setCurrentError(err.response.data.error));
+    }
+  };
+
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Please enter a valid email
+        </ErrorHelper>
+      );
+    }
+  };
+
+  renderConfirmPasswordError = () => {
+    let { confirmPasswordError } = this.state;
+    if (confirmPasswordError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Passwords do not match
+        </ErrorHelper>
+      );
+    }
+  };
+
+  renderGithubSection = () => {
+    if (this.state.hasGithub) {
+      return (
+        <OAuthButton onClick={this.githubRedirect}>
+          <IconWrapper>
+            <Icon src={github} />
+            Sign up with GitHub
+          </IconWrapper>
+        </OAuthButton>
+      );
+    }
+  };
+
+  renderGoogleSection = () => {
+    if (this.state.hasGoogle) {
+      return (
+        <OAuthButton onClick={this.googleRedirect}>
+          <IconWrapper>
+            <StyledGoogleIcon />
+            Sign up with Google
+          </IconWrapper>
+        </OAuthButton>
+      );
+    }
+  };
+
+  renderBasicSection = () => {
+    let {
+      email,
+      password,
+      confirmPassword,
+      emailError,
+      confirmPasswordError,
+    } = this.state;
+
+    if (this.state.hasBasic) {
+      return (
+        <div>
+          <InputWrapper>
+            <Input
+              type="email"
+              placeholder="Email"
+              value={email}
+              onChange={(e: ChangeEvent<HTMLInputElement>) =>
+                this.setState({ email: e.target.value, emailError: false })
+              }
+              valid={!emailError}
+            />
+            {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>
+            <Input
+              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}
+        >
+          <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>
+    );
+  }
+}
+
+Register.contextType = Context;
+
+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 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`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  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;
+  display: flex;
+  background: #ffffff;
+  align-items: center;
+  border-radius: 3px;
+  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;
+  margin-top: -20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  position: relative;
+  align-items: center;
+`;
+
+const StyledRegister = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 204 - 419
dashboard/src/main/auth/Register.tsx

@@ -1,288 +1,230 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
-import logo from "assets/logo.png";
-import github from "assets/github-icon.png";
-import GoogleIcon from "assets/GoogleIcon";
 
-import api from "shared/api";
-import { emailRegex } from "shared/regex";
-import { Context } from "shared/Context";
+import github from "assets/github-icon.png";
+import logo from "assets/logo.png";
 
-type PropsType = {
+import GoogleIcon from "assets/GoogleIcon";
+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 Props = {
   authenticate: () => void;
 };
 
-type StateType = {
-  email: string;
-  password: string;
-  confirmPassword: string;
-  emailError: boolean;
-  confirmPasswordError: boolean;
-  hasGithub: boolean;
-  hasGoogle: boolean;
-  hasBasic: boolean;
-};
-
-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);
-  }
+const getWindowDimensions = () => {
+  const { innerWidth: width, innerHeight: height } = window;
+  return { width, height };
+}
 
-  githubRedirect = () => {
+const Register: React.FC<Props> = ({
+  authenticate,
+}) => {
+  const [firstName, setFirstName] = useState("");
+  const [lastName, setLastName] = useState("");
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [hasGithub, setHasGithub] = useState(true);
+  const [hasGoogle, setHasGoogle] = useState(true);
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  useEffect(() => {
+    const handleResize = () => {
+      setWindowDimensions(getWindowDimensions());
+    };
+
+    window.addEventListener('resize', handleResize);
+    return () => window.removeEventListener('resize', handleResize);
+  }, []);
+
+  const 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;
-
-    if (!emailRegex.test(email)) {
-      this.setState({ emailError: true });
-    }
-
-    if (confirmPassword !== password) {
-      this.setState({ confirmPasswordError: true });
-    }
-
-    // Check for valid input
-    if (emailRegex.test(email) && confirmPassword === password) {
-      // Attempt user registration
-      api
-        .registerUser(
-          "",
-          {
-            email: email,
-            password: password,
-          },
-          {}
-        )
-        .then((res: any) => {
-          if (res?.data?.redirect) {
-            window.location.href = res.data.redirect;
-          } else {
-            setUser(res?.data?.id, res?.data?.email);
-            authenticate();
-          }
-        })
-        .catch((err) => setCurrentError(err.response.data.error));
-    }
-  };
-
-  renderEmailError = () => {
-    let { emailError } = this.state;
-    if (emailError) {
+  const renderOAuthSection = () => {
+    if (hasGithub || hasGoogle) {
       return (
-        <ErrorHelper>
-          <div />
-          Please enter a valid email
-        </ErrorHelper>
+        <>
+          <Container row>
+            {hasGithub && (
+              <OAuthButton onClick={githubRedirect}>
+                <Icon src={github} />
+                Sign up with GitHub
+              </OAuthButton>
+            )}
+            {hasGithub && hasGoogle && (
+              <Spacer inline x={2} />
+            )}
+            {hasGoogle && (
+              <OAuthButton onClick={githubRedirect}>
+                <StyledGoogleIcon />
+                Sign up with Google
+              </OAuthButton>
+            )}
+          </Container>
+          <OrWrapper>
+            <Line />
+            <Or>or</Or>
+          </OrWrapper>
+        </>
       );
     }
   };
 
-  renderConfirmPasswordError = () => {
-    let { confirmPasswordError } = this.state;
-    if (confirmPasswordError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Passwords do not match
-        </ErrorHelper>
-      );
-    }
-  };
+  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} />
+        {renderOAuthSection()}
+        <Container row>
+          <Input
+            placeholder="First name"
+            label="First name"
+            value={firstName}
+            setValue={setFirstName}
+            width="100%"
+            height="40px"
+          />
+          <Spacer inline x={2} />
+          <Input
+            placeholder="Last name"
+            label="Last name"
+            value={lastName}
+            setValue={setLastName}
+            width="100%"
+            height="40px"
+          />
+        </Container>
+        <Spacer y={1} />
+        <Input
+          placeholder="Email"
+          label="Email"
+          value={email}
+          setValue={setEmail}
+          width="100%"
+          height="40px"
+        />
+        <Spacer y={1} />
+        <Input
+          placeholder="Password"
+          label="Password"
+          value={password}
+          setValue={setPassword}
+          width="100%"
+          height="40px"
+          type="password"
+        />
+        <Spacer height="30px" />
+        <Button onClick={authenticate} width="100%" height="40px">
+          Continue
+        </Button>
+        <Spacer y={1} />
+        <Text 
+          size={13}
+          color="helper"
+        >
+          Already have an account? <Link to="/login">Log in</Link>
+        </Text>
+      </Wrapper>
+    </StyledRegister>
+  );
+};
 
-  renderGithubSection = () => {
-    if (this.state.hasGithub) {
-      return (
-        <OAuthButton onClick={this.githubRedirect}>
-          <IconWrapper>
-            <Icon src={github} />
-            Sign up with GitHub
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
-  };
+export default Register;
 
-  renderGoogleSection = () => {
-    if (this.state.hasGoogle) {
-      return (
-        <OAuthButton onClick={this.googleRedirect}>
-          <IconWrapper>
-            <StyledGoogleIcon />
-            Sign up with Google
-          </IconWrapper>
-        </OAuthButton>
-      );
-    }
-  };
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+`;
 
-  renderBasicSection = () => {
-    let {
-      email,
-      password,
-      confirmPassword,
-      emailError,
-      confirmPasswordError,
-    } = this.state;
+const CheckRow = styled.div`
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb;
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: #4797ff;
+  }
+`;
 
-    if (this.state.hasBasic) {
-      return (
-        <div>
-          <InputWrapper>
-            <Input
-              type="email"
-              placeholder="Email"
-              value={email}
-              onChange={(e: ChangeEvent<HTMLInputElement>) =>
-                this.setState({ email: e.target.value, emailError: false })
-              }
-              valid={!emailError}
-            />
-            {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>
-            <Input
-              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>
-      );
-    }
-  };
+const Shiny = styled.span`
+  background-image: linear-gradient(225deg, #fff, #7980ff);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+`;
 
-  render() {
-    return (
-      <StyledRegister>
-        <LoginPanel
-          hasBasic={this.state.hasBasic}
-          numOAuth={+this.state.hasGithub + +this.state.hasGoogle}
-        >
-          <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>
-    );
-  }
-}
+const Jumbotron = styled.div`
+  font-size: 32px;
+  font-weight: 500;
+  line-height: 1.5;
+`;
 
-Register.contextType = Context;
+const Logo = styled.img`
+  height: 24px;
+  user-select: none;
+`;
 
-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 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 +236,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 +277,4 @@ const StyledRegister = 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
         );
       }
 

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov