浏览代码

Porter FE (#4452)

Co-authored-by: sunguroku <65516095+sunguroku@users.noreply.github.com>
jusrhee 2 年之前
父节点
当前提交
697a2617ad

+ 9 - 0
dashboard/src/assets/gift.svg

@@ -0,0 +1,9 @@
+<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.2909 34V10.8566M16.5826 9.46825C16.8707 9.53255 17.1743 9.45374 17.3817 9.24746C17.5892 9.04118 17.6683 8.73915 17.6038 8.45275C17.3452 7.40232 16.3926 3.88491 15.4145 2.91224C14.1959 1.70041 12.2126 1.69536 11 2.90119C9.78751 4.10694 9.79242 6.07925 11.0111 7.29116C12.0052 8.27975 15.5263 9.21113 16.5826 9.46825ZM18.5402 8.45267C18.4756 8.73918 18.5548 9.0411 18.7622 9.24738C18.9697 9.45366 19.2734 9.53235 19.5614 9.46817C20.6177 9.21102 24.1548 8.26375 25.1329 7.29108C26.3515 6.07925 26.3566 4.10694 25.144 2.90111C23.9315 1.69536 21.9482 1.70024 20.7295 2.91215C19.7354 3.90074 18.7988 7.40227 18.5402 8.45267ZM3.16364 18.9568H32.8364C33.479 18.9568 34 18.4387 34 17.7996V12.0138C34 11.3747 33.479 10.8566 32.8364 10.8566H3.16364C2.52098 10.8566 2 11.3747 2 12.0138V17.7996C2 18.4387 2.52098 18.9568 3.16364 18.9568ZM31.0909 18.9568V32.8428C31.0909 33.4819 30.5699 34 29.9273 34H6.07273C5.43007 34 4.90909 33.4819 4.90909 32.8428V18.9568H31.0909Z" stroke="url(#paint0_linear_1699_18)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_1699_18" x1="14.5" y1="5" x2="33.4613" y2="50.1509" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F8F8F8"/>
+<stop offset="1" stop-color="#666666" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>

+ 18 - 16
dashboard/src/components/porter/Select.tsx

@@ -20,6 +20,7 @@ type Props = {
   setValue?: (value: string) => void;
   prefix?: React.ReactNode;
   width?: string;
+  height?: string;
 };
 
 const Select: React.FC<Props> = ({
@@ -32,11 +33,12 @@ const Select: React.FC<Props> = ({
   setValue,
   prefix,
   width,
+  height,
 }) => {
   return (
     <Div width={width}>
       {label && <Label color={labelColor}>{label}</Label>}
-      <SelectWrapper isDisabled={disabled ?? false}>
+      <SelectWrapper isDisabled={disabled ?? false} height={height}>
         {prefix && (
           <>
             <Prefix>{prefix}</Prefix>
@@ -128,11 +130,11 @@ const Error = styled.div`
   }
 `;
 
-const SelectWrapper = styled.div<{ isDisabled: boolean }>`
+const SelectWrapper = styled.div<{ isDisabled: boolean; height?: string }>`
   position: relative;
   padding-left: 10px;
   padding-right: 28px;
-  height: 30px;
+  height: ${(props) => props.height || "30px"};
   transition: all 0.2s;
   background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
@@ -152,18 +154,18 @@ const SelectWrapper = styled.div<{ isDisabled: boolean }>`
   }
 
   ${(props) =>
-    !props.isDisabled ?
-    css`
-      :hover {
-        border: 1px solid #7a7b80;
-      }
-    ` : 
-    css`
-      color: #ffffff55;
-      > img {
-        opacity: 0.5;
-      }
-    `}
+    !props.isDisabled
+      ? css`
+          :hover {
+            border: 1px solid #7a7b80;
+          }
+        `
+      : css`
+          color: #ffffff55;
+          > img {
+            opacity: 0.5;
+          }
+        `}
 `;
 
 const SelectLayer = styled.select<{
@@ -176,7 +178,7 @@ const SelectLayer = styled.select<{
   left: 0;
   width: 100%;
   height: 100%;
-  cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   background: none;
   appearance: none;
   opacity: 0;

+ 119 - 0
dashboard/src/main/auth/InfoPanel.tsx

@@ -0,0 +1,119 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+
+import Container from "components/porter/Container";
+import Image from "components/porter/Image";
+import Spacer from "components/porter/Spacer";
+
+import gift from "assets/gift.svg";
+import logo from "assets/logo.png";
+
+const InfoPanel: React.FC = () => {
+  if (window.location.hostname === "cloud.porter.run") {
+    return (
+      <Wrapper>
+        <Container row>
+          <a href="https://porter.run">
+            <Logo src={logo} />
+          </a>
+          <Badge>Cloud</Badge>
+        </Container>
+        <Spacer y={2} />
+        <Jumbotron>
+          Deploy and scale <Shiny>effortlessly</Shiny> with Porter
+        </Jumbotron>
+        <Spacer y={2} />
+        <CheckRow>
+          <Image src={gift} />
+          <Spacer inline width="10px" /> $5 in free credits on sign-up
+        </CheckRow>
+        <Spacer y={0.5} />
+        <CheckRow>
+          <i className="material-icons">done</i> Instantly deploy from any Git
+          repo
+        </CheckRow>
+        <Spacer y={0.5} />
+        <CheckRow>
+          <i className="material-icons">done</i> Eject at any time to your own
+          AWS/Azure/GCP account
+        </CheckRow>
+      </Wrapper>
+    );
+  }
+  return (
+    <Wrapper>
+      <a href="https://porter.run">
+        <Logo src={logo} />
+      </a>
+      <Spacer y={2} />
+      <Jumbotron>
+        Deploy and scale <Shiny>effortlessly</Shiny> with Porter
+      </Jumbotron>
+      <Spacer y={2} />
+      <CheckRow>
+        <i className="material-icons">done</i> 14 day free trial
+      </CheckRow>
+      <Spacer y={0.5} />
+      <CheckRow>
+        <i className="material-icons">done</i> Generous startup program for
+        seed-stage companies
+      </CheckRow>
+      <Spacer y={0.5} />
+      <CheckRow>
+        <i className="material-icons">done</i> Bring your own cloud and use your
+        credits
+      </CheckRow>
+    </Wrapper>
+  );
+};
+
+export default InfoPanel;
+
+const Badge = styled.div`
+  margin-left: 17px;
+  margin-top: -6px;
+  background: ${(props) => props.theme.clickable};
+  padding: 5px 10px;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+`;
+
+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;
+  }
+`;
+
+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 Wrapper = styled.div`
+  width: 500px;
+  margin-top: -20px;
+  position: relative;
+  padding: 25px;
+  border-radius: 5px;
+  font-size: 13px;
+`;

+ 53 - 50
dashboard/src/main/auth/Login.tsx

@@ -1,25 +1,24 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
-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";
-
 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 Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import Link from "components/porter/Link";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { emailRegex } from "shared/regex";
+import blog from "assets/blog.png";
+import community from "assets/community.png";
+import docs from "assets/docs.png";
+import github from "assets/github-icon.png";
+import GoogleIcon from "assets/GoogleIcon";
+import logo from "assets/logo.png";
 
 type Props = {
   authenticate: () => void;
@@ -28,11 +27,9 @@ type Props = {
 const getWindowDimensions = () => {
   const { innerWidth: width, innerHeight: height } = window;
   return { width, height };
-}
+};
 
-const Login: React.FC<Props> = ({
-  authenticate,
-}) => {
+const Login: React.FC<Props> = ({ authenticate }) => {
   const { setUser, setCurrentError } = useContext(Context);
   const [email, setEmail] = useState("");
   const [password, setPassword] = useState("");
@@ -42,7 +39,9 @@ const Login: React.FC<Props> = ({
   const [hasGithub, setHasGithub] = useState(true);
   const [hasGoogle, setHasGoogle] = useState(false);
   const [hasResetPassword, setHasResetPassword] = useState(true);
-  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+  const [windowDimensions, setWindowDimensions] = useState(
+    getWindowDimensions()
+  );
 
   const handleLogin = (): void => {
     if (!emailRegex.test(email)) {
@@ -50,11 +49,8 @@ const Login: React.FC<Props> = ({
     } else if (password === "") {
       setCredentialError(true);
     } else {
-      api.logInUser(
-        "",
-        { email: email, password: password },
-        {}
-      )
+      api
+        .logInUser("", { email, password }, {})
         .then((res) => {
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
@@ -63,7 +59,9 @@ const Login: React.FC<Props> = ({
             authenticate();
           }
         })
-        .catch((err) => setCurrentError(err.response.data.error));
+        .catch((err) => {
+          setCurrentError(err.response.data.error);
+        });
     }
   };
 
@@ -74,7 +72,7 @@ const Login: React.FC<Props> = ({
   const handleKeyDown = (e: any) => {
     if (e.key === "Enter") {
       handleLogin();
-    };
+    }
   };
 
   // Manually re-register event listener on email/password change
@@ -87,34 +85,36 @@ const Login: React.FC<Props> = ({
   }, [email, password]);
 
   useEffect(() => {
-
     // Get capabilities to case on login methods
-    api.getMetadata("", {}, {})
+    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));
+      .catch((err) => {
+        console.log(err);
+      });
 
     const urlParams = new URLSearchParams(window.location.search);
     const emailFromCLI = urlParams.get("email");
     emailFromCLI && setEmail(emailFromCLI);
 
-    window.addEventListener('resize', handleResize);
+    window.addEventListener("resize", handleResize);
     return () => {
-      window.removeEventListener('resize', handleResize);
+      window.removeEventListener("resize", handleResize);
     };
   }, []);
 
   const githubRedirect = () => {
-    let redirectUrl = `/api/oauth/login/github`;
+    const redirectUrl = `/api/oauth/login/github`;
     window.location.href = redirectUrl;
   };
 
   const googleRedirect = () => {
-    let redirectUrl = `/api/oauth/login/google`;
+    const redirectUrl = `/api/oauth/login/google`;
     window.location.href = redirectUrl;
   };
 
@@ -122,9 +122,12 @@ const Login: React.FC<Props> = ({
     <StyledLogin>
       {windowDimensions.width > windowDimensions.height && (
         <Wrapper>
-          <a href="https://porter.run">
-            <Logo src={logo} />
-          </a>
+          <Container row>
+            <a href="https://porter.run">
+              <Logo src={logo} />
+            </a>
+            <Badge>Cloud</Badge>
+          </Container>
           <Spacer y={2} />
           <Jumbotron>
             <Shiny>Welcome back to Porter</Shiny>
@@ -137,10 +140,6 @@ const Login: React.FC<Props> = ({
           <LinkRow to="https://porter.run/blog" 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>
@@ -152,9 +151,7 @@ const Login: React.FC<Props> = ({
             <Spacer y={2} />
           </Flex>
         )}
-        <Heading isAtTop>
-          Log in to your Porter account
-        </Heading>
+        <Heading isAtTop>Log in to your Porter account</Heading>
         <Spacer y={1} />
         {(hasGithub || hasGoogle) && (
           <>
@@ -165,9 +162,7 @@ const Login: React.FC<Props> = ({
                   Log in with GitHub
                 </OAuthButton>
               )}
-              {hasGithub && hasGoogle && (
-                <Spacer inline x={2} />
-              )}
+              {hasGithub && hasGoogle && <Spacer inline x={2} />}
               {hasGoogle && (
                 <OAuthButton onClick={googleRedirect}>
                   <StyledGoogleIcon />
@@ -227,11 +222,10 @@ const Login: React.FC<Props> = ({
           </>
         )}
         <Spacer y={1} />
-        <Text
-          size={13}
-          color="helper"
-        >
-          Don't have an account?<Spacer width="5px" inline /><Link to="/register">Sign up</Link>
+        <Text size={13} color="helper">
+          Don't have an account?
+          <Spacer width="5px" inline />
+          <Link to="/register">Sign up</Link>
         </Text>
       </Wrapper>
     </StyledLogin>
@@ -240,6 +234,15 @@ const Login: React.FC<Props> = ({
 
 export default Login;
 
+const Badge = styled.div`
+  margin-left: 17px;
+  margin-top: -6px;
+  background: ${(props) => props.theme.clickable};
+  padding: 5px 10px;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+`;
+
 const ForgotPassword = styled.div`
   position: absolute;
   right: 0;

+ 178 - 127
dashboard/src/main/auth/Register.tsx

@@ -1,22 +1,23 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } 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 { emailRegex } from "shared/regex";
-import { Context } from "shared/Context";
-
 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";
 import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { emailRegex } from "shared/regex";
+import github from "assets/github-icon.png";
+import GoogleIcon from "assets/GoogleIcon";
+import logo from "assets/logo.png";
+
+import InfoPanel from "./InfoPanel";
 
 type Props = {
   authenticate: () => void;
@@ -25,11 +26,9 @@ type Props = {
 const getWindowDimensions = () => {
   const { innerWidth: width, innerHeight: height } = window;
   return { width, height };
-}
+};
 
-const Register: React.FC<Props> = ({
-  authenticate,
-}) => {
+const Register: React.FC<Props> = ({ authenticate }) => {
   const { setUser, setCurrentError } = useContext(Context);
   const [firstName, setFirstName] = useState("");
   const [firstNameError, setFirstNameError] = useState(false);
@@ -45,16 +44,22 @@ const Register: React.FC<Props> = ({
   const [hasBasic, setHasBasic] = useState(true);
   const [hasGithub, setHasGithub] = useState(true);
   const [hasGoogle, setHasGoogle] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+  const [windowDimensions, setWindowDimensions] = useState(
+    getWindowDimensions()
+  );
   const [buttonDisabled, setButtonDisabled] = useState(false);
 
-  const [chosenReferralOption, setChosenReferralOption] = useState<string>("(None provided)");
+  const [chosenReferralOption, setChosenReferralOption] =
+    useState<string>("(None provided)");
   const [referralOtherText, setReferralOtherText] = useState<string>("");
 
   const referralOptions = [
     { value: "(None provided)", label: "Please select an option:" },
     { value: "Email", label: "Email" },
-    { value: "Word of mouth", label: "Word of mouth (friend, colleague, etc.)" },
+    {
+      value: "Word of mouth",
+      label: "Word of mouth (friend, colleague, etc.)",
+    },
     { value: "YC", label: "YC" },
     { value: "YC Startup School", label: "YC Startup School" },
     { value: "Facebook", label: "Facebook" },
@@ -64,18 +69,19 @@ const Register: React.FC<Props> = ({
     { value: "LinkedIn", label: "LinkedIn" },
     { value: "Porter blog", label: "Porter blog" },
     { value: "Other", label: "Other" },
-  ]
+  ];
 
   const handleRegister = (): void => {
+    const isHosted = window.location.hostname === "cloud.porter.run";
     if (!emailRegex.test(email)) {
       setEmailError(true);
     }
 
-    if (firstName === "") {
+    if (firstName === "" && !isHosted) {
       setFirstNameError(true);
     }
 
-    if (lastName === "") {
+    if (lastName === "" && !isHosted) {
       setLastNameError(true);
     }
 
@@ -83,12 +89,13 @@ const Register: React.FC<Props> = ({
       setPasswordError(true);
     }
 
-    if (companyName === "") {
+    if (companyName === "" && !isHosted) {
       setCompanyNameError(true);
     }
 
     // Check for valid input
     if (
+      !isHosted &&
       emailRegex.test(email) &&
       firstName !== "" &&
       lastName !== "" &&
@@ -102,12 +109,15 @@ const Register: React.FC<Props> = ({
         .registerUser(
           "",
           {
-            email: email,
-            password: password,
+            email,
+            password,
             first_name: firstName,
             last_name: lastName,
             company_name: companyName,
-            referral_method: chosenReferralOption === "Other" ? `Other: ${referralOtherText}` : chosenReferralOption,
+            referral_method:
+              chosenReferralOption === "Other"
+                ? `Other: ${referralOtherText}`
+                : chosenReferralOption,
           },
           {}
         )
@@ -117,14 +127,14 @@ const Register: React.FC<Props> = ({
           } else {
             setUser(res?.data?.id, res?.data?.email);
             authenticate();
-            
+
             try {
               window.dataLayer?.push({
-                event: 'sign-up',
+                event: "sign-up",
                 data: {
-                  method: 'email',
-                  email: res?.data?.email
-                }
+                  method: "email",
+                  email: res?.data?.email,
+                },
               });
             } catch (err) {
               console.log(err);
@@ -138,7 +148,60 @@ const Register: React.FC<Props> = ({
         .catch((err) => {
           console.log("registration:", err);
           if (err.response?.data?.error) {
-            setCurrentError(err.response.data.error)
+            setCurrentError(err.response.data.error);
+          } else {
+            location.reload();
+          }
+          setButtonDisabled(false);
+        });
+    } else if (isHosted && emailRegex.test(email) && password !== "") {
+      setButtonDisabled(true);
+
+      // Attempt user registration
+      api
+        .registerUser(
+          "",
+          {
+            email,
+            password,
+            first_name: email,
+            last_name: email,
+            company_name: email,
+            referral_method:
+              chosenReferralOption === "Other"
+                ? `Other: ${referralOtherText}`
+                : chosenReferralOption,
+          },
+          {}
+        )
+        .then((res: any) => {
+          if (res?.data?.redirect) {
+            window.location.href = res.data.redirect;
+          } else {
+            setUser(res?.data?.id, res?.data?.email);
+            authenticate();
+
+            try {
+              window.dataLayer?.push({
+                event: "sign-up",
+                data: {
+                  method: "email",
+                  email: res?.data?.email,
+                },
+              });
+            } catch (err) {
+              console.log(err);
+            }
+          }
+
+          // Temp
+          location.reload();
+          setButtonDisabled(false);
+        })
+        .catch((err) => {
+          console.log("registration:", err);
+          if (err.response?.data?.error) {
+            setCurrentError(err.response.data.error);
           } else {
             location.reload();
           }
@@ -154,7 +217,7 @@ const Register: React.FC<Props> = ({
   const handleKeyDown = (e: any) => {
     if (e.key === "Enter") {
       handleRegister();
-    };
+    }
   };
 
   // Manually re-register event listener on email/password change
@@ -167,67 +230,48 @@ const Register: React.FC<Props> = ({
   }, [email, password, firstName, lastName]);
 
   useEffect(() => {
-    let qs = window.location.search;
-    let urlParams = new URLSearchParams(qs);
-    let email = urlParams.get('email');
+    const qs = window.location.search;
+    const urlParams = new URLSearchParams(qs);
+    const email = urlParams.get("email");
 
     if (email) {
       setEmail(email);
       setDisabled(true);
     }
-
   }, []);
 
   useEffect(() => {
-
     // Get capabilities to case on login methods
-    api.getMetadata("", {}, {})
+    api
+      .getMetadata("", {}, {})
       .then((res) => {
         setHasBasic(res.data?.basic_login);
         setHasGithub(res.data?.github_login);
         setHasGoogle(res.data?.google_login);
       })
-      .catch((err) => console.log(err));
+      .catch((err) => {
+        console.log(err);
+      });
 
-    window.addEventListener('resize', handleResize);
-    return () => window.removeEventListener('resize', handleResize);
+    window.addEventListener("resize", handleResize);
+    return () => {
+      window.removeEventListener("resize", handleResize);
+    };
   }, []);
 
   const githubRedirect = () => {
-    let redirectUrl = `/api/oauth/login/github`;
+    const redirectUrl = `/api/oauth/login/github`;
     window.location.href = redirectUrl;
   };
 
   const googleRedirect = () => {
-    let redirectUrl = `/api/oauth/login/google`;
+    const redirectUrl = `/api/oauth/login/google`;
     window.location.href = redirectUrl;
   };
 
   return (
     <StyledRegister>
-      {windowDimensions.width > windowDimensions.height && (
-        <Wrapper>
-          <a href="https://porter.run">
-            <Logo src={logo} />
-          </a>
-          <Spacer y={2} />
-          <Jumbotron>
-            Deploy and scale <Shiny>effortlessly</Shiny> with Porter
-          </Jumbotron>
-          <Spacer y={2} />
-          <CheckRow>
-            <i className="material-icons">done</i> 14 day free trial
-          </CheckRow>
-          <Spacer y={0.5} />
-          <CheckRow>
-            <i className="material-icons">done</i>  Generous startup program for seed-stage companies
-          </CheckRow>
-          <Spacer y={0.5} />
-          <CheckRow>
-            <i className="material-icons">done</i> Bring your own cloud and use your credits
-          </CheckRow>
-        </Wrapper>
-      )}
+      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
       <Wrapper>
         {windowDimensions.width <= windowDimensions.height && (
           <Flex>
@@ -237,11 +281,9 @@ const Register: React.FC<Props> = ({
             <Spacer y={2} />
           </Flex>
         )}
-        <Heading isAtTop>
-          Create your Porter account
-        </Heading>
+        <Heading isAtTop>Create your Porter account</Heading>
         <Spacer y={1} />
-        {((hasGithub || hasGoogle) && !disabled) && (
+        {(hasGithub || hasGoogle) && !disabled && (
           <>
             <Container row>
               {hasGithub && (
@@ -250,9 +292,7 @@ const Register: React.FC<Props> = ({
                   Sign up with GitHub
                 </OAuthButton>
               )}
-              {hasGithub && hasGoogle && (
-                <Spacer inline x={2} />
-              )}
+              {hasGithub && hasGoogle && <Spacer inline x={2} />}
               {hasGoogle && (
                 <OAuthButton onClick={googleRedirect}>
                   <StyledGoogleIcon />
@@ -270,57 +310,61 @@ const Register: React.FC<Props> = ({
         )}
         {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>
+            {window.location.hostname !== "cloud.porter.run" && (
+              <>
+                <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
-                  placeholder="Last name"
-                  label="Last name"
-                  value={lastName}
+                  placeholder="Company name"
+                  label="Company name"
+                  value={companyName}
                   setValue={(x) => {
-                    setLastName(x);
-                    setLastNameError(false);
+                    setCompanyName(x);
+                    setCompanyNameError(false);
                   }}
                   width="100%"
                   height="40px"
-                  error={(lastNameError && "Last name cannot be blank")}
+                  error={companyNameError && ""}
                 />
-                {!lastNameError && firstNameError && (
-                  <Spacer height="27px" />
-                )}
-              </RowWrapper>
-            </Container>
-            <Spacer y={1} />
-            <Input
-              placeholder="Company name"
-              label="Company name"
-              value={companyName}
-              setValue={(x) => {
-                setCompanyName(x);
-                setCompanyNameError(false);
-              }}
-              width="100%"
-              height="40px"
-              error={(companyNameError && "")}
-            />
-            <Spacer y={1} />
+                <Spacer y={1} />
+              </>
+            )}
             <Input
               type="email"
               placeholder="Email"
@@ -332,7 +376,7 @@ const Register: React.FC<Props> = ({
               }}
               width="100%"
               height="40px"
-              error={(emailError && "Please enter a valid email")}
+              error={emailError && "Please enter a valid email"}
               disabled={disabled}
             />
             <Spacer y={1} />
@@ -344,13 +388,14 @@ const Register: React.FC<Props> = ({
               width="100%"
               height="40px"
               type="password"
-              error={(passwordError && "")}
+              error={passwordError && ""}
             />
             <Spacer y={1} />
             <Text color="helper">(Optional) How did you hear about us?</Text>
             <Spacer y={0.5} />
             <Select
               width="100%"
+              height="40px"
               options={referralOptions}
               setValue={setChosenReferralOption}
               value={chosenReferralOption}
@@ -361,13 +406,20 @@ const Register: React.FC<Props> = ({
                 <FeedbackInput
                   autoFocus={true}
                   value={referralOtherText}
-                  onChange={(e) => setReferralOtherText(e.target.value)}
+                  onChange={(e) => {
+                    setReferralOtherText(e.target.value);
+                  }}
                   placeholder="Tell us more..."
                 />
               </>
             )}
             <Spacer y={1} />
-            <Button disabled={buttonDisabled} onClick={handleRegister} width="100%" height="40px">
+            <Button
+              disabled={buttonDisabled}
+              onClick={handleRegister}
+              width="100%"
+              height="40px"
+            >
               Continue
             </Button>
           </>
@@ -375,11 +427,10 @@ const Register: React.FC<Props> = ({
         {!disabled && (
           <>
             <Spacer y={1} />
-            <Text
-              size={13}
-              color="helper"
-            >
-              Already have an account?<Spacer width="5px" inline /><Link to="/login">Log in</Link>
+            <Text size={13} color="helper">
+              Already have an account?
+              <Spacer width="5px" inline />
+              <Link to="/login">Log in</Link>
             </Text>
           </>
         )}

+ 49 - 78
dashboard/src/main/auth/SetInfo.tsx

@@ -1,21 +1,22 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } 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 { emailRegex } from "shared/regex";
-import { Context } from "shared/Context";
-
 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 Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import Link from "components/porter/Link";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { emailRegex } from "shared/regex";
+import github from "assets/github-icon.png";
+import GoogleIcon from "assets/GoogleIcon";
+import logo from "assets/logo.png";
+
+import InfoPanel from "./InfoPanel";
 
 type Props = {
   authenticate: () => void;
@@ -25,12 +26,9 @@ type Props = {
 const getWindowDimensions = () => {
   const { innerWidth: width, innerHeight: height } = window;
   return { width, height };
-}
+};
 
-const SetInfo: React.FC<Props> = ({
-  authenticate,
-  handleLogOut,
-}) => {
+const SetInfo: React.FC<Props> = ({ authenticate, handleLogOut }) => {
   const { user, setCurrentError } = useContext(Context);
   const [firstName, setFirstName] = useState("");
   const [firstNameError, setFirstNameError] = useState(false);
@@ -38,7 +36,9 @@ const SetInfo: React.FC<Props> = ({
   const [lastNameError, setLastNameError] = useState(false);
   const [companyName, setCompanyName] = useState("");
   const [companyNameError, setCompanyNameError] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+  const [windowDimensions, setWindowDimensions] = useState(
+    getWindowDimensions()
+  );
 
   const handleResize = () => {
     setWindowDimensions(getWindowDimensions());
@@ -57,43 +57,42 @@ const SetInfo: React.FC<Props> = ({
       setCompanyNameError(true);
     }
 
-    if (
-      firstName !== "" &&
-      lastName !== "" &&
-      companyName !== ""
-    ) {
-      api.updateUserInfo(
-        "",
-        { 
-          first_name: firstName,
-          last_name: lastName,
-          company_name: companyName,
-        },
-        { id: user.id }
-      )
+    if (firstName !== "" && lastName !== "" && companyName !== "") {
+      api
+        .updateUserInfo(
+          "",
+          {
+            first_name: firstName,
+            last_name: lastName,
+            company_name: companyName,
+          },
+          { id: user.id }
+        )
         .then((res: any) => {
           authenticate();
 
           try {
             window.dataLayer?.push({
-              event: 'sign-up',
+              event: "sign-up",
               data: {
-                method: 'github',
-                email: user?.email
-              }
+                method: "github",
+                email: user?.email,
+              },
             });
           } catch (err) {
             console.log(err);
           }
         })
-        .catch((err) => setCurrentError(err));
+        .catch((err) => {
+          setCurrentError(err);
+        });
     }
   };
 
   const handleKeyDown = (e: any) => {
     if (e.key === "Enter") {
       finishAccountSetup();
-    };
+    }
   };
 
   // Manually re-register event listener on email/password change
@@ -106,35 +105,15 @@ const SetInfo: React.FC<Props> = ({
   }, [firstName, lastName, companyName]);
 
   useEffect(() => {
-    window.addEventListener('resize', handleResize);
-    return () => window.removeEventListener('resize', handleResize);
+    window.addEventListener("resize", handleResize);
+    return () => {
+      window.removeEventListener("resize", handleResize);
+    };
   }, []);
 
   return (
     <StyledRegister>
-      {windowDimensions.width > windowDimensions.height && (
-        <Wrapper>
-          <Logo src={logo} onClick={() => {
-            window.location.href = "https://porter.run"
-          }}/>
-          <Spacer y={2} />
-          <Jumbotron>
-            Deploy and scale <Shiny>effortlessly</Shiny> with Porter
-          </Jumbotron>
-          <Spacer y={2} />
-          <CheckRow>
-            <i className="material-icons">done</i> Generous startup program for seed-stage companies
-          </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>
-      )}
+      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
       <Wrapper>
         {windowDimensions.width <= windowDimensions.height && (
           <Flex>
@@ -142,9 +121,7 @@ const SetInfo: React.FC<Props> = ({
             <Spacer y={2} />
           </Flex>
         )}
-        <Heading isAtTop>
-          Finish setting up your account
-        </Heading>
+        <Heading isAtTop>Finish setting up your account</Heading>
         <Spacer y={1} />
         <Container row>
           <RowWrapper>
@@ -158,11 +135,9 @@ const SetInfo: React.FC<Props> = ({
               }}
               width="100%"
               height="40px"
-              error={(firstNameError && "First name cannot be blank")}
+              error={firstNameError && "First name cannot be blank"}
             />
-            {!firstNameError && lastNameError && (
-              <Spacer height="27px" />
-            )}
+            {!firstNameError && lastNameError && <Spacer height="27px" />}
           </RowWrapper>
           <Spacer inline x={2} />
           <RowWrapper>
@@ -176,11 +151,9 @@ const SetInfo: React.FC<Props> = ({
               }}
               width="100%"
               height="40px"
-              error={(lastNameError && "Last name cannot be blank")}
+              error={lastNameError && "Last name cannot be blank"}
             />
-            {!lastNameError && firstNameError && (
-              <Spacer height="27px" />
-            )}
+            {!lastNameError && firstNameError && <Spacer height="27px" />}
           </RowWrapper>
         </Container>
         <Spacer y={1} />
@@ -194,18 +167,16 @@ const SetInfo: React.FC<Props> = ({
           }}
           width="100%"
           height="40px"
-          error={(companyNameError && "")}
+          error={companyNameError && ""}
         />
         <Spacer height="30px" />
         <Button onClick={finishAccountSetup} width="100%" height="40px">
           Continue
         </Button>
         <Spacer y={1} />
-        <Text 
-          size={13}
-          color="helper"
-        >
-          Want to use a different login method? <Link onClick={handleLogOut}>Log out</Link>
+        <Text size={13} color="helper">
+          Want to use a different login method?{" "}
+          <Link onClick={handleLogOut}>Log out</Link>
         </Text>
       </Wrapper>
     </StyledRegister>

+ 31 - 48
dashboard/src/main/auth/VerifyEmail.tsx

@@ -1,20 +1,21 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } 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";
-
 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 Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import Link from "components/porter/Link";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import github from "assets/github-icon.png";
+import GoogleIcon from "assets/GoogleIcon";
+import logo from "assets/logo.png";
+
+import InfoPanel from "./InfoPanel";
 
 type Props = {
   handleLogOut: () => void;
@@ -23,55 +24,40 @@ type Props = {
 const getWindowDimensions = () => {
   const { innerWidth: width, innerHeight: height } = window;
   return { width, height };
-}
+};
 
-const Register: React.FC<Props> = ({
-  handleLogOut,
-}) => {
+const Register: React.FC<Props> = ({ handleLogOut }) => {
   const { user, setCurrentError } = useContext(Context);
   const [submitted, setSubmitted] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+  const [windowDimensions, setWindowDimensions] = useState(
+    getWindowDimensions()
+  );
 
   const handleResize = () => {
     setWindowDimensions(getWindowDimensions());
   };
 
   useEffect(() => {
-    window.addEventListener('resize', handleResize);
-    return () => window.removeEventListener('resize', handleResize);
+    window.addEventListener("resize", handleResize);
+    return () => {
+      window.removeEventListener("resize", handleResize);
+    };
   }, []);
 
   const handleSendEmail = (): void => {
-    api.createEmailVerification("", {}, {})
+    api
+      .createEmailVerification("", {}, {})
       .then((res) => {
         setSubmitted(true);
       })
-      .catch((err) => setCurrentError(err.response.data.error));
+      .catch((err) => {
+        setCurrentError(err.response.data.error);
+      });
   };
 
   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 startup program for seed-stage companies 
-          </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>
-      )}
+      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
       <Wrapper>
         {windowDimensions.width <= windowDimensions.height && (
           <Flex>
@@ -79,9 +65,7 @@ const Register: React.FC<Props> = ({
             <Spacer y={2} />
           </Flex>
         )}
-        <Heading isAtTop>
-          Verify your email
-        </Heading>
+        <Heading isAtTop>Verify your email</Heading>
         <Spacer y={1} />
         {submitted ? (
           <>
@@ -121,11 +105,10 @@ const Register: React.FC<Props> = ({
           </>
         )}
         <Spacer y={1} />
-        <Text 
-          size={13}
-          color="helper"
-        >
-          Want to use a different email?<Spacer inline width="5px" /><Link onClick={handleLogOut}>Log out</Link>
+        <Text size={13} color="helper">
+          Want to use a different email?
+          <Spacer inline width="5px" />
+          <Link onClick={handleLogOut}>Log out</Link>
         </Text>
       </Wrapper>
     </StyledRegister>
@@ -256,4 +239,4 @@ const StyledRegister = styled.div`
   top: 0;
   left: 0;
   background: #111114;
-`;
+`;

+ 186 - 167
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -1,37 +1,37 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
-import styled from "styled-components";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import _ from "lodash";
 import { Link, LinkProps } from "react-router-dom";
+import styled from "styled-components";
+
+import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
+import Loading from "components/Loading";
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
+import Fieldset from "components/porter/Fieldset";
+import Icon from "components/porter/Icon";
+import PorterLink from "components/porter/Link";
+import SearchBar from "components/porter/SearchBar";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Toggle from "components/porter/Toggle";
 
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { search } from "shared/search";
+import { readableDate } from "shared/string_utils";
 import applications from "assets/applications.svg";
 import box from "assets/box.png";
+import calendar from "assets/calendar-number.svg";
 import github from "assets/github.png";
-import time from "assets/time.png";
-import healthy from "assets/status-healthy.png";
 import grid from "assets/grid.png";
 import list from "assets/list.png";
-import letter from "assets/vector.svg";
-import calendar from "assets/calendar-number.svg";
 import notFound from "assets/not-found.png";
-
-import { Context } from "shared/Context";
-import { search } from "shared/search";
-import api from "shared/api";
-import { readableDate } from "shared/string_utils";
+import healthy from "assets/status-healthy.png";
+import time from "assets/time.png";
+import letter from "assets/vector.svg";
 
 import DashboardHeader from "../cluster-dashboard/DashboardHeader";
-import Container from "components/porter/Container";
-import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import SearchBar from "components/porter/SearchBar";
-import Toggle from "components/porter/Toggle";
-import PorterLink from "components/porter/Link";
-import Loading from "components/Loading";
-import Fieldset from "components/porter/Fieldset";
-import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
-import Icon from "components/porter/Icon";
-import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
 
 type Props = {};
 
@@ -53,8 +53,9 @@ const namespaceBlacklist = [
   "monitoring",
 ];
 
-const AppDashboard: React.FC<Props> = ({ }) => {
-  const { currentProject, currentCluster, setFeaturePreview } = useContext(Context);
+const AppDashboard: React.FC<Props> = ({}) => {
+  const { currentProject, currentCluster, setFeaturePreview } =
+    useContext(Context);
   const [apps, setApps] = useState([]);
   const [charts, setCharts] = useState([]);
   const [error, setError] = useState(null);
@@ -93,8 +94,8 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       );
       const apps = res.data;
       const timeRes = await Promise.all(
-        apps.map((app: any) => {
-          return api.getCharts(
+        apps.map(async (app: any) => {
+          return await api.getCharts(
             "<token>",
             {
               limit: 1,
@@ -120,7 +121,7 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       );
       apps.forEach((app: any, i: number) => {
         if (timeRes?.[i]?.data?.[0]?.info?.last_deployed != null) {
-          app["last_deployed"] = readableDate(
+          app.last_deployed = readableDate(
             timeRes[i].data[0].info.last_deployed
           );
         }
@@ -145,7 +146,9 @@ const AppDashboard: React.FC<Props> = ({ }) => {
         {app.repo_name ? (
           <Container row>
             <SmallIcon opacity="0.6" src={github} />
-            <Text size={13} color="#ffffff44">{app.repo_name}</Text>
+            <Text size={13} color="#ffffff44">
+              {app.repo_name}
+            </Text>
           </Container>
         ) : (
           <Container row>
@@ -154,7 +157,9 @@ const AppDashboard: React.FC<Props> = ({ }) => {
               height="18px"
               src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
             />
-            <Text truncate={true} size={13} color="#ffffff44">{app.image_repo_uri}</Text>
+            <Text truncate={true} size={13} color="#ffffff44">
+              {app.image_repo_uri}
+            </Text>
           </Container>
         )}
       </>
@@ -166,7 +171,7 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       await api.updateStackStep(
         "<token>",
         {
-          step: 'stack-launch-start'
+          step: "stack-launch-start",
         },
         {
           cluster_id: currentCluster.id,
@@ -176,11 +181,10 @@ const AppDashboard: React.FC<Props> = ({ }) => {
     } catch (err) {
       // TODO: handle error
     }
-  }
-
+  };
 
   const renderIcon = (b: string, size?: string) => {
-    var src = box;
+    let src = box;
     if (b) {
       const bp = b.split(",")[0]?.split("/")[1];
       switch (bp) {
@@ -201,7 +205,13 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       }
     }
     return (
-      <>{size === "larger" ? <Icon height="16px" src={src} /> : <Icon height="18px" src={src} />}</>
+      <>
+        {size === "larger" ? (
+          <Icon height="16px" src={src} />
+        ) : (
+          <Icon height="18px" src={src} />
+        )}
+      </>
     );
   };
 
@@ -215,140 +225,149 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       />
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
         <ClusterProvisioningPlaceholder />
-      ) : (
-        apps.length === 0 ? (
-          isLoading ?
-            (<Loading offset="-150px" />) : (
-              <DashboardPlaceholder>
-                <Text size={16}>
-                  No apps have been deployed yet
-                </Text>
-                <Spacer y={0.5} />
-                <Text color={"helper"}>
-                  Get started by deploying your app.
-                </Text>
-                <Spacer y={1} />
-                <PorterLink to="/apps/new/app">
-                  <Button alt onClick={async () => updateStackStartedStep()} height="35px">
-                    Deploy app <Spacer inline x={1} /> <i className="material-icons" style={{ fontSize: '18px' }}>east</i>
-                  </Button>
-                </PorterLink>
-              </DashboardPlaceholder>
-            )
+      ) : apps.length === 0 ? (
+        isLoading ? (
+          <Loading offset="-150px" />
         ) : (
-          <>
-            <Container row spaced>
-              <SearchBar
-                value={searchValue}
-                setValue={(x) => {
-                  if (x === "open_sesame") {
-                    setFeaturePreview(true);
-                  }
-                  setSearchValue(x);
-                }}
-                placeholder="Search applications . . ."
-                width="100%"
-              />
-              <Spacer inline x={2} />
-              <Toggle
-                items={[
-                  { label: <ToggleIcon src={calendar} />, value: "calendar" },
-                  { label: <ToggleIcon src={letter} />, value: "letter" },
-                ]}
-                active={sort}
-                setActive={setSort}
-              />
-              <Spacer inline x={1} />
-
-              <Toggle
-                items={[
-                  { label: <ToggleIcon src={grid} />, value: "grid" },
-                  { label: <ToggleIcon src={list} />, value: "list" },
-                ]}
-                active={view}
-                setActive={setView}
-              />
-
-              <Spacer inline x={2} />
-              <PorterLink to="/apps/new/app">
-                <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
-                  <I className="material-icons">add</I> New application
-                </Button>
-              </PorterLink>
-            </Container>
+          <DashboardPlaceholder>
+            <Text size={16}>No apps have been deployed yet</Text>
+            <Spacer y={0.5} />
+            <Text color={"helper"}>Get started by deploying your app.</Text>
             <Spacer y={1} />
+            <PorterLink to="/apps/new/app">
+              <Button
+                alt
+                onClick={async () => {
+                  await updateStackStartedStep();
+                }}
+                height="35px"
+              >
+                Deploy app <Spacer inline x={1} />{" "}
+                <i className="material-icons" style={{ fontSize: "18px" }}>
+                  east
+                </i>
+              </Button>
+            </PorterLink>
+          </DashboardPlaceholder>
+        )
+      ) : (
+        <>
+          <Container row spaced>
+            <SearchBar
+              value={searchValue}
+              setValue={(x) => {
+                if (x === "open_sesame") {
+                  setFeaturePreview(true);
+                }
+                setSearchValue(x);
+              }}
+              placeholder="Search applications . . ."
+              width="100%"
+            />
+            <Spacer inline x={2} />
+            <Toggle
+              items={[
+                { label: <ToggleIcon src={calendar} />, value: "calendar" },
+                { label: <ToggleIcon src={letter} />, value: "letter" },
+              ]}
+              active={sort}
+              setActive={setSort}
+            />
+            <Spacer inline x={1} />
+
+            <Toggle
+              items={[
+                { label: <ToggleIcon src={grid} />, value: "grid" },
+                { label: <ToggleIcon src={list} />, value: "list" },
+              ]}
+              active={view}
+              setActive={setView}
+            />
 
-            {filteredApps.length === 0 ? (
-              <Fieldset>
-                <Container row>
-                  <PlaceholderIcon src={notFound} />
-                  <Text color="helper">No matching apps were found.</Text>
-                </Container>
-              </Fieldset>
-            ) : (isLoading ? (
-              <Loading offset="-150px" />
-            ) : view === "grid" ? (
-              <GridList>
-                {(filteredApps ?? []).map((app: any, i: number) => {
-                  if (!namespaceBlacklist.includes(app.name)) {
-                    return (
-                      <Link to={`/apps/${app.name}`} key={i}>
-                        <Block>
-                          <Container row>
-                            {renderIcon(app["buildpacks"])}
-                            <Spacer inline width="12px" />
-                            <Text size={14}>{app.name}</Text>
-                            <Spacer inline x={2} />
-                          </Container>
-                          <StatusIcon src={healthy} />
+            <Spacer inline x={2} />
+            <PorterLink to="/apps/new/app">
+              <Button
+                onClick={async () => {
+                  await updateStackStartedStep();
+                }}
+                height="30px"
+                width="160px"
+              >
+                <I className="material-icons">add</I> New application
+              </Button>
+            </PorterLink>
+          </Container>
+          <Spacer y={1} />
+
+          {filteredApps.length === 0 ? (
+            <Fieldset>
+              <Container row>
+                <PlaceholderIcon src={notFound} />
+                <Text color="helper">No matching apps were found.</Text>
+              </Container>
+            </Fieldset>
+          ) : isLoading ? (
+            <Loading offset="-150px" />
+          ) : view === "grid" ? (
+            <GridList>
+              {(filteredApps ?? []).map((app: any, i: number) => {
+                if (!namespaceBlacklist.includes(app.name)) {
+                  return (
+                    <Link to={`/apps/${app.name}`} key={i}>
+                      <Block>
+                        <Container row>
+                          {renderIcon(app.buildpacks)}
+                          <Spacer inline width="12px" />
+                          <Text size={14}>{app.name}</Text>
+                          <Spacer inline x={2} />
+                        </Container>
+                        <StatusIcon src={healthy} />
+                        {renderSource(app)}
+                        <Container row>
+                          <SmallIcon opacity="0.4" src={time} />
+                          <Text size={13} color="#ffffff44">
+                            {app.last_deployed}
+                          </Text>
+                        </Container>
+                      </Block>
+                    </Link>
+                  );
+                }
+              })}
+            </GridList>
+          ) : (
+            <List>
+              {(filteredApps ?? []).map((app: any, i: number) => {
+                if (!namespaceBlacklist.includes(app.name)) {
+                  return (
+                    <Link to={`/apps/${app.name}`} key={i}>
+                      <Row>
+                        <Container row>
+                          <Spacer inline width="1px" />
+                          {renderIcon(app.buildpacks, "larger")}
+                          <Spacer inline width="12px" />
+                          <Text size={14}>{app.name}</Text>
+                          <Spacer inline x={1} />
+                          <Icon height="16px" src={healthy} />
+                        </Container>
+                        <Spacer height="15px" />
+                        <Container row>
                           {renderSource(app)}
-                          <Container row>
-                            <SmallIcon opacity="0.4" src={time} />
-                            <Text size={13} color="#ffffff44">{app.last_deployed}</Text>
-                          </Container>
-                        </Block>
-                      </Link>
-                    );
-                  }
-                })}
-              </GridList>
-            ) : (
-              <List>
-                {(filteredApps ?? []).map((app: any, i: number) => {
-                  if (!namespaceBlacklist.includes(app.name)) {
-                    return (
-                      <Link to={`/apps/${app.name}`} key={i}>
-                        <Row>
-                          <Container row>
-                            <Spacer inline width="1px" />
-                            {renderIcon(app["buildpacks"], "larger")}
-                            <Spacer inline width="12px" />
-                            <Text size={14}>
-                              {app.name}
-                            </Text>
-                            <Spacer inline x={1} />
-                            <Icon height="16px" src={healthy} />
-                          </Container>
-                          <Spacer height="15px" />
-                          <Container row>
-                            {renderSource(app)}
-                            <Spacer inline x={1} />
-                            <SmallIcon opacity="0.4" src={time} />
-                            <Text size={13} color="#ffffff44">
-                              {app.last_deployed}
-                            </Text>
-                          </Container>
-                        </Row>
-                      </Link>
-                    );
-                  }
-                })}
-              </List>
-            ))}
-          </>
-        )
-      )
-      }
+                          <Spacer inline x={1} />
+                          <SmallIcon opacity="0.4" src={time} />
+                          <Text size={13} color="#ffffff44">
+                            {app.last_deployed}
+                          </Text>
+                        </Container>
+                      </Row>
+                    </Link>
+                  );
+                }
+              })}
+            </List>
+          )}
+        </>
+      )}
       <Spacer y={5} />
     </StyledAppDashboard>
   );
@@ -451,5 +470,5 @@ const CentralContainer = styled.div`
   display: flex;
   flex-direction: column;
   justify-content: left;
-  align-items: left;   
-`;
+  align-items: left;
+`;

+ 53 - 34
dashboard/src/main/home/app-dashboard/apps/Apps.tsx

@@ -7,10 +7,13 @@ import { z } from "zod";
 
 import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
 import Loading from "components/Loading";
+import Banner from "components/porter/Banner";
 import Button from "components/porter/Button";
 import Container from "components/porter/Container";
 import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
+import Image from "components/porter/Image";
 import PorterLink from "components/porter/Link";
+import Link from "components/porter/Link";
 import Modal from "components/porter/Modal";
 import SearchBar from "components/porter/SearchBar";
 import Spacer from "components/porter/Spacer";
@@ -28,6 +31,7 @@ import { Context } from "shared/Context";
 import { useDeploymentTarget } from "shared/DeploymentTargetContext";
 import applicationGrad from "assets/application-grad.svg";
 import calendar from "assets/calendar-number.svg";
+import gift from "assets/gift.svg";
 import grid from "assets/grid.png";
 import list from "assets/list.png";
 import pull_request from "assets/pull_request_icon.svg";
@@ -212,31 +216,28 @@ const Apps: React.FC = () => {
       }
 
       return (
-        <DashboardPlaceholder>
-          <Text size={16}>No applications have been created yet</Text>
-          <Spacer y={0.5} />
-          <Text color={"helper"}>Get started by creating an application.</Text>
-          <Spacer y={1} />
-          {currentProject?.billing_enabled && !hasPaymentEnabled ? (
-            <Button
-              alt
-              onClick={() => {
-                setShowBillingModal(true);
-              }}
-              height="35px"
-            >
-              Create a new application
-              <Spacer inline x={1} />{" "}
-              <i className="material-icons" style={{ fontSize: "18px" }}>
-                east
-              </i>
-            </Button>
-          ) : (
-            <PorterLink to="/apps/new/app">
+        <>
+          {currentProject?.sandbox_enabled && (
+            <>
+              <Banner icon={<Image src={gift} />}>
+                $5 of Porter credits have automatically been credited to your
+                account.
+              </Banner>
+              <Spacer y={1} />
+            </>
+          )}
+          <DashboardPlaceholder>
+            <Text size={16}>No applications have been created yet</Text>
+            <Spacer y={0.5} />
+            <Text color={"helper"}>
+              Get started by creating an application.
+            </Text>
+            <Spacer y={1} />
+            {currentProject?.billing_enabled && !hasPaymentEnabled ? (
               <Button
                 alt
-                onClick={async () => {
-                  await updateAppStep({ step: "stack-launch-start" });
+                onClick={() => {
+                  setShowBillingModal(true);
                 }}
                 height="35px"
               >
@@ -246,17 +247,35 @@ const Apps: React.FC = () => {
                   east
                 </i>
               </Button>
-            </PorterLink>
-          )}
-          {showBillingModal && (
-            <BillingModal
-              back={() => setShowBillingModal(false)}
-              onCreate={() => {
-                history.push("/apps/new/app");
-              }}
-            />
-          )}
-        </DashboardPlaceholder>
+            ) : (
+              <PorterLink to="/apps/new/app">
+                <Button
+                  alt
+                  onClick={async () => {
+                    await updateAppStep({ step: "stack-launch-start" });
+                  }}
+                  height="35px"
+                >
+                  Create a new application
+                  <Spacer inline x={1} />{" "}
+                  <i className="material-icons" style={{ fontSize: "18px" }}>
+                    east
+                  </i>
+                </Button>
+              </PorterLink>
+            )}
+            {showBillingModal && (
+              <BillingModal
+                back={() => {
+                  setShowBillingModal(false);
+                }}
+                onCreate={() => {
+                  history.push("/apps/new/app");
+                }}
+              />
+            )}
+          </DashboardPlaceholder>
+        </>
       );
     }
 

+ 21 - 1
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -6,6 +6,7 @@ import Button from "components/porter/Button";
 import Container from "components/porter/Container";
 import Fieldset from "components/porter/Fieldset";
 import Icon from "components/porter/Icon";
+import Image from "components/porter/Image";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import {
@@ -17,6 +18,7 @@ import {
 
 import { Context } from "shared/Context";
 import cardIcon from "assets/credit-card.svg";
+import gift from "assets/gift.svg";
 import trashIcon from "assets/trash.png";
 
 import BillingModal from "../modals/BillingModal";
@@ -44,12 +46,30 @@ function BillingPage(): JSX.Element {
 
   if (shouldCreate) {
     return (
-      <BillingModal onCreate={onCreate} back={() => setShouldCreate(false)} />
+      <BillingModal
+        onCreate={onCreate}
+        back={() => {
+          setShouldCreate(false);
+        }}
+      />
     );
   }
 
   return (
     <>
+      <Text size={16}>Porter credit balance</Text>
+      <Spacer y={1} />
+      <Text color="helper">
+        View the amount of Porter credits you have available to spend on
+        resources within this project.
+      </Text>
+      <Spacer y={1} />
+      <Container row>
+        <Image src={gift} style={{ marginTop: "-2px" }} />
+        <Spacer inline x={1} />
+        <Text size={20}>$ 5.00</Text>
+      </Container>
+      <Spacer y={2} />
       <Text size={16}>Payment methods</Text>
       <Spacer y={1} />
       <Text color="helper">