aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts/components')
-rw-r--r--packages/website/ts/components/aboutPageLayout.tsx71
-rw-r--r--packages/website/ts/components/animatedChatIcon.tsx106
-rw-r--r--packages/website/ts/components/animatedCompassIcon.tsx53
-rw-r--r--packages/website/ts/components/banner.tsx147
-rw-r--r--packages/website/ts/components/blockIconLink.tsx84
-rw-r--r--packages/website/ts/components/button.tsx102
-rw-r--r--packages/website/ts/components/chapter_link.tsx15
-rw-r--r--packages/website/ts/components/definition.tsx145
-rw-r--r--packages/website/ts/components/dropdowns/dropdown_developers.tsx180
-rw-r--r--packages/website/ts/components/dropdowns/dropdown_products.tsx48
-rw-r--r--packages/website/ts/components/footer.tsx368
-rw-r--r--packages/website/ts/components/hamburger.tsx72
-rw-r--r--packages/website/ts/components/header.tsx252
-rw-r--r--packages/website/ts/components/hero.tsx152
-rw-r--r--packages/website/ts/components/heroAnimation.tsx123
-rw-r--r--packages/website/ts/components/heroImage.tsx27
-rw-r--r--packages/website/ts/components/icon.tsx72
-rw-r--r--packages/website/ts/components/image.tsx20
-rw-r--r--packages/website/ts/components/layout.tsx177
-rw-r--r--packages/website/ts/components/link.tsx64
-rw-r--r--packages/website/ts/components/logo.tsx41
-rw-r--r--packages/website/ts/components/mobileNav.tsx121
-rw-r--r--packages/website/ts/components/modals/input.tsx95
-rw-r--r--packages/website/ts/components/modals/modal_contact.tsx278
-rw-r--r--packages/website/ts/components/newLayout.tsx152
-rw-r--r--packages/website/ts/components/newsletter_form.tsx202
-rw-r--r--packages/website/ts/components/sections/landing/about.tsx81
-rw-r--r--packages/website/ts/components/sections/landing/clients.tsx113
-rw-r--r--packages/website/ts/components/sections/landing/cta.tsx29
-rw-r--r--packages/website/ts/components/sections/landing/hero.tsx31
-rw-r--r--packages/website/ts/components/separator.tsx7
-rw-r--r--packages/website/ts/components/siteWrap.tsx149
-rw-r--r--packages/website/ts/components/slider/slider.tsx177
-rw-r--r--packages/website/ts/components/text.tsx83
34 files changed, 3623 insertions, 214 deletions
diff --git a/packages/website/ts/components/aboutPageLayout.tsx b/packages/website/ts/components/aboutPageLayout.tsx
new file mode 100644
index 000000000..86a94ae2b
--- /dev/null
+++ b/packages/website/ts/components/aboutPageLayout.tsx
@@ -0,0 +1,71 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { Button } from 'ts/@next/components/button';
+import { ChapterLink } from 'ts/@next/components/chapter_link';
+import { Column, Section } from 'ts/@next/components/newLayout';
+import { SiteWrap } from 'ts/@next/components/siteWrap';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+
+import { addFadeInAnimation } from 'ts/@next/constants/animations';
+import { WebsitePaths } from 'ts/types';
+
+interface Props {
+ title: string;
+ description: React.ReactNode | string;
+ linkLabel?: string;
+ href?: string;
+ to?: string;
+ children?: React.ReactNode;
+}
+
+export const AboutPageLayout = (props: Props) => (
+ <SiteWrap theme="light">
+ <Section isFlex={true} maxWidth="1170px" wrapWidth="100%">
+ <Column>
+ <ChapterLink to={WebsitePaths.AboutMission}>Mission</ChapterLink>
+ <ChapterLink to={WebsitePaths.AboutTeam}>Team</ChapterLink>
+ <ChapterLink to={WebsitePaths.AboutPress}>Press</ChapterLink>
+ <ChapterLink to={WebsitePaths.AboutJobs}>Jobs</ChapterLink>
+ </Column>
+
+ <Column width="70%" maxWidth="800px">
+ <Column width="100%" maxWidth="680px">
+ <AnimatedHeading size="medium">{props.title}</AnimatedHeading>
+
+ <AnimatedParagraph size="medium" marginBottom="60px" isMuted={0.65}>
+ {props.description}
+ </AnimatedParagraph>
+
+ {props.linkLabel &&
+ (props.href || props.to) && (
+ <AnimatedLink
+ to={props.to}
+ href={props.href}
+ target={!_.isUndefined(props.href) ? '_blank' : undefined}
+ isWithArrow={true}
+ isAccentColor={true}
+ >
+ {props.linkLabel}
+ </AnimatedLink>
+ )}
+ </Column>
+ </Column>
+ </Section>
+
+ {props.children}
+ </SiteWrap>
+);
+
+const AnimatedHeading = styled(Heading)`
+ ${addFadeInAnimation('0.5s')};
+`;
+
+const AnimatedParagraph = styled(Paragraph)`
+ ${addFadeInAnimation('0.5s', '0.15s')};
+`;
+
+const AnimatedLink = styled(Button)`
+ ${addFadeInAnimation('0.6s', '0.3s')};
+`;
diff --git a/packages/website/ts/components/animatedChatIcon.tsx b/packages/website/ts/components/animatedChatIcon.tsx
new file mode 100644
index 000000000..9a86e244c
--- /dev/null
+++ b/packages/website/ts/components/animatedChatIcon.tsx
@@ -0,0 +1,106 @@
+import * as React from 'react';
+import styled, { keyframes } from 'styled-components';
+
+export const AnimatedChatIcon = () => (
+ <svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <mask id="mask30" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="150" height="150">
+ <circle cx="75" cy="75" r="73" fill="#00AE99" stroke="#00AE99" stroke-width="3" />
+ </mask>
+
+ <g mask="url(#mask30)">
+ <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3" />
+
+ <Rays>
+ <path vector-effect="non-scaling-stroke" d="M76 37H137.5" stroke="#00AE99" stroke-width="3" />
+ <path
+ vector-effect="non-scaling-stroke"
+ d="M37 73.5L37 12M113 137.5L113 75"
+ stroke="#00AE99"
+ stroke-width="3"
+ />
+ <path vector-effect="non-scaling-stroke" d="M13 113H71.5" stroke="#00AE99" stroke-width="3" />
+ <path
+ vector-effect="non-scaling-stroke"
+ d="M49.087 47.5264L92.574 4.03932"
+ stroke="#00AE99"
+ stroke-width="3"
+ />
+ <path
+ vector-effect="non-scaling-stroke"
+ d="M47.3192 100.913L3.8321 57.4259M146.314 92.4277L102.12 48.2335"
+ stroke="#00AE99"
+ stroke-width="3"
+ />
+ <path
+ vector-effect="non-scaling-stroke"
+ d="M58.2793 145.814L101.766 102.327"
+ stroke="#00AE99"
+ stroke-width="3"
+ />
+ </Rays>
+
+ <Bubble>
+ <path
+ vector-effect="non-scaling-stroke"
+ d="M113 75C113 85.3064 108.897 94.6546 102.235 101.5C98.4048 105.436 71 132.5 71 132.5V112.792C51.8933 110.793 37 94.6359 37 75C37 54.0132 54.0132 37 75 37C95.9868 37 113 54.0132 113 75Z"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ </Bubble>
+
+ <Dot delay={0} vector-effect="non-scaling-stroke" cx="75" cy="75" r="4" stroke="#00AE99" strokeWidth="3" />
+ <Dot
+ delay={4.4}
+ vector-effect="non-scaling-stroke"
+ cx="91"
+ cy="75"
+ r="4"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ <Dot
+ delay={-4.6}
+ vector-effect="non-scaling-stroke"
+ cx="59"
+ cy="75"
+ r="4"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ </g>
+ </svg>
+);
+
+const scale = keyframes`
+ 0% { transform: scale(1.2) }
+ 15% { transform: scale(1) }
+ 85% { transform: scale(1) }
+ 100% { transform: scale(1.2) }
+`;
+
+const fadeInOut = keyframes`
+ 0%, 30%, 50%, 100% {
+ transform: initial;
+ }
+
+ 40% {
+ transform: translateY(-5px);
+ }
+`;
+
+const Bubble = styled.g`
+ animation: ${scale} 4s infinite cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transform-origin: 50% 50%;
+`;
+
+const Rays = styled.g`
+ animation: ${scale} 4s infinite cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transform-origin: 50% 50%;
+`;
+
+const Dot =
+ styled.circle <
+ { delay: number } >
+ `
+ animation: ${fadeInOut} 4s ${props => `${props.delay}s`} infinite;
+`;
diff --git a/packages/website/ts/components/animatedCompassIcon.tsx b/packages/website/ts/components/animatedCompassIcon.tsx
new file mode 100644
index 000000000..5388f95ca
--- /dev/null
+++ b/packages/website/ts/components/animatedCompassIcon.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react';
+import styled, { keyframes } from 'styled-components';
+
+export const AnimatedCompassIcon = () => (
+ <svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <g>
+ <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3" />
+ <circle cx="75" cy="75" r="58" stroke="#00AE99" stroke-width="3" />
+ <Needle
+ d="M62.9792 62.9792L36.6447 113.355L87.0208 87.0208M62.9792 62.9792L113.355 36.6447L87.0208 87.0208M62.9792 62.9792L87.0208 87.0208"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+
+ <Dial>
+ <path d="M75 2V17M75 133V148" stroke="#00AE99" stroke-width="3" />
+ <path d="M2 75L17 75M133 75L148 75" stroke="#00AE99" stroke-width="3" />
+ <path d="M11.7801 38.5L24.7705 46M125.229 104L138.22 111.5" stroke="#00AE99" stroke-width="3" />
+ <path d="M38.5001 11.7801L46.0001 24.7705M104 125.229L111.5 138.22" stroke="#00AE99" stroke-width="3" />
+ <path d="M111.5 11.7801L104 24.7705M46 125.229L38.5 138.22" stroke="#00AE99" stroke-width="3" />
+ <path d="M138.22 38.5L125.229 46M24.7705 104L11.7801 111.5" stroke="#00AE99" stroke-width="3" />
+ </Dial>
+ </g>
+ </svg>
+);
+
+const point = keyframes`
+ 0% { transform: rotate(0deg) }
+ 20% { transform: rotate(10deg) }
+ 30% { transform: rotate(30deg) }
+ 60% { transform: rotate(-20deg) }
+ 80% { transform: rotate(-20deg) }
+ 100% { transform: rotate(0deg) }
+`;
+
+const rotate = keyframes`
+ 0% { transform: rotate(0deg) }
+ 20% { transform: rotate(-10deg) }
+ 30% { transform: rotate(-30deg) }
+ 60% { transform: rotate(20deg) }
+ 80% { transform: rotate(20deg) }
+ 100% { transform: rotate(0deg) }
+`;
+
+const Needle = styled.path`
+ animation: ${point} 5s infinite;
+ transform-origin: 50% 50%;
+`;
+
+const Dial = styled.g`
+ animation: ${rotate} 5s infinite;
+ transform-origin: 50% 50%;
+`;
diff --git a/packages/website/ts/components/banner.tsx b/packages/website/ts/components/banner.tsx
new file mode 100644
index 000000000..6c4d94dc5
--- /dev/null
+++ b/packages/website/ts/components/banner.tsx
@@ -0,0 +1,147 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { colors } from 'ts/style/colors';
+
+import { Button } from 'ts/@next/components/button';
+import { ThemeInterface } from 'ts/@next/components/siteWrap';
+import { Paragraph } from 'ts/@next/components/text';
+
+import { Column, Section } from 'ts/@next/components/newLayout';
+
+interface Props {
+ heading?: string;
+ subline?: string;
+ mainCta?: CTAButton;
+ secondaryCta?: CTAButton;
+ theme?: ThemeInterface;
+}
+
+interface CTAButton {
+ text: string;
+ href?: string;
+ onClick?: () => void;
+ shouldOpenInNewTab?: boolean;
+}
+
+interface BorderProps {
+ isBottom?: boolean;
+}
+
+export const Banner: React.StatelessComponent<Props> = (props: Props) => {
+ const { heading, subline, mainCta, secondaryCta } = props;
+ return (
+ <CustomSection bgColor={colors.brandDark} isFlex={true} flexBreakpoint="900px" paddingMobile="120px 0">
+ <Border />
+ <Border isBottom={true} />
+
+ <Column>
+ <CustomHeading>{heading}</CustomHeading>
+
+ {subline && (
+ <Paragraph color={colors.white} isMuted={0.5} isNoMargin={true}>
+ {subline}
+ </Paragraph>
+ )}
+ </Column>
+ <Column>
+ <ButtonWrap>
+ {mainCta && (
+ <Button
+ color={colors.white}
+ isTransparent={false}
+ href={mainCta.href}
+ target={mainCta.shouldOpenInNewTab ? '_blank' : ''}
+ >
+ {mainCta.text}
+ </Button>
+ )}
+
+ {secondaryCta && (
+ <Button
+ color={colors.white}
+ href={secondaryCta.href}
+ onClick={secondaryCta.onClick}
+ isTransparent={true}
+ >
+ {secondaryCta.text}
+ </Button>
+ )}
+ </ButtonWrap>
+ </Column>
+ </CustomSection>
+ );
+};
+
+const CustomSection = styled(Section)`
+ color: ${colors.white};
+ margin-top: 30px;
+
+ @media (max-width: 900px) {
+ text-align: center;
+
+ p {
+ margin-bottom: 30px;
+ }
+
+ div:last-child {
+ margin-bottom: 0;
+ }
+ }
+`;
+
+const CustomHeading = styled.h2`
+ font-size: 34px;
+ font-weight: 400;
+ margin-bottom: 10px @media (max-width: 768px) {
+ font-size: 30px;
+ }
+`;
+
+const ButtonWrap = styled.div`
+ display: inline-block;
+
+ @media (min-width: 768px) {
+ * + * {
+ margin-left: 15px;
+ }
+ }
+
+ @media (max-width: 768px) {
+ a,
+ button {
+ display: block;
+ width: 220px;
+ }
+
+ * + * {
+ margin-top: 15px;
+ }
+ }
+`;
+
+// Note let's refactor this
+// is it absolutely necessary to have a stateless component
+// to pass props down into the styled icon?
+const Border =
+ styled.div <
+ BorderProps >
+ `
+ position: absolute;
+ background-image: ${props =>
+ props.isBottom ? 'url(/images/@next/banner/bottomofcta.png);' : 'url(/images/@next/banner/topofcta.png);'};
+ background-position: ${props => (props.isBottom ? 'left top' : 'left bottom')};
+ left: 0;
+ width: calc(100% + 214px);
+ height: 40px;
+ top: ${props => !props.isBottom && 0};
+ bottom: ${props => props.isBottom && 0};
+ transform: translate(-112px);
+
+ @media (max-width: 768px) {
+ width: calc(100% + 82px);
+ height: 40px;
+ transform: translate(-41px);
+ background-size: auto 80px;
+ }
+`;
diff --git a/packages/website/ts/components/blockIconLink.tsx b/packages/website/ts/components/blockIconLink.tsx
new file mode 100644
index 000000000..8d66a4afa
--- /dev/null
+++ b/packages/website/ts/components/blockIconLink.tsx
@@ -0,0 +1,84 @@
+import { History, Location } from 'history';
+import * as React from 'react';
+import { match, withRouter } from 'react-router-dom';
+import styled from 'styled-components';
+
+import { Button } from 'ts/@next/components/button';
+import { Icon } from 'ts/@next/components/icon';
+
+interface BaseComponentProps {
+ icon?: string;
+ iconComponent?: React.ReactNode;
+ title: string;
+ linkLabel: string;
+ linkUrl?: string;
+ linkAction?: () => void;
+ history: History;
+ location: Location;
+ match: match<any>;
+}
+
+class BaseComponent extends React.PureComponent<BaseComponentProps> {
+ public onClick = (): void => {
+ const { linkAction, linkUrl } = this.props;
+
+ if (linkAction) {
+ linkAction();
+ } else {
+ this.props.history.push(linkUrl);
+ }
+ };
+
+ public render(): React.ReactNode {
+ const { icon, iconComponent, linkUrl, linkAction, title, linkLabel } = this.props;
+
+ return (
+ <Wrap onClick={this.onClick}>
+ <div>
+ <Icon name={icon} component={iconComponent} size="large" margin={[0, 0, 'default', 0]} />
+
+ <Title>{title}</Title>
+
+ <Button isWithArrow={true} isTransparent={true} href={linkUrl} onClick={linkAction}>
+ {linkLabel}
+ </Button>
+ </div>
+ </Wrap>
+ );
+ }
+}
+
+export const BlockIconLink = withRouter<BaseComponentProps>(BaseComponent);
+
+const Wrap = styled.div`
+ width: calc(50% - 15px);
+ height: 400px;
+ padding: 40px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ transition: background-color 0.25s;
+ background-color: ${props => props.theme.lightBgColor};
+ cursor: pointer;
+
+ a,
+ button {
+ pointer-events: none;
+ }
+
+ @media (max-width: 900px) {
+ width: 100%;
+ margin-top: 30px;
+ }
+
+ &:hover {
+ background-color: #002d28;
+ }
+`;
+
+const Title = styled.h2`
+ font-size: 20px;
+ margin-bottom: 30px;
+ color: ${props => props.theme.linkColor};
+`;
diff --git a/packages/website/ts/components/button.tsx b/packages/website/ts/components/button.tsx
new file mode 100644
index 000000000..348f1b7b4
--- /dev/null
+++ b/packages/website/ts/components/button.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react';
+import { Link as ReactRouterLink } from 'react-router-dom';
+import styled from 'styled-components';
+
+import { ThemeInterface } from 'ts/@next/components/siteWrap';
+
+import { colors } from 'ts/style/colors';
+
+interface ButtonInterface {
+ bgColor?: string;
+ color?: string;
+ children?: React.ReactNode | string;
+ isTransparent?: boolean;
+ isNoBorder?: boolean;
+ isNoPadding?: boolean;
+ isWithArrow?: boolean;
+ isAccentColor?: boolean;
+ hasIcon?: boolean | string;
+ isInline?: boolean;
+ href?: string;
+ type?: string;
+ target?: string;
+ to?: string;
+ onClick?: () => any;
+ theme?: ThemeInterface;
+ shouldUseAnchorTag?: boolean;
+}
+
+export const Button = (props: ButtonInterface) => {
+ const { children, href, isWithArrow, to, shouldUseAnchorTag, target } = props;
+ let linkElem;
+
+ if (href || shouldUseAnchorTag) {
+ linkElem = 'a';
+ }
+ if (to) {
+ linkElem = ReactRouterLink;
+ }
+
+ const Component = linkElem ? ButtonBase.withComponent<any>(linkElem) : ButtonBase;
+ const targetProp = href && target ? { target } : {};
+
+ return (
+ <Component {...props} {...targetProp}>
+ {children}
+
+ {isWithArrow && (
+ <svg width="16" height="15" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M4.484.246l.024 1.411 8.146.053L.817 13.547l.996.996L13.65 2.706l.052 8.146 1.412.024L15.045.315 4.484.246z" />
+ </svg>
+ )}
+ </Component>
+ );
+};
+
+const ButtonBase =
+ styled.button <
+ ButtonInterface >
+ `
+ appearance: none;
+ border: 1px solid transparent;
+ display: inline-block;
+ background-color: ${props => props.bgColor || colors.brandLight};
+ background-color: ${props => (props.isTransparent || props.isWithArrow) && 'transparent'};
+ border-color: ${props => props.isTransparent && !props.isWithArrow && 'rgba(255, 255, 255, .4)'};
+ color: ${props => (props.isAccentColor ? props.theme.linkColor : props.color || props.theme.textColor)};
+ padding: ${props => !props.isNoPadding && !props.isWithArrow && '18px 30px'};
+ white-space: ${props => props.isWithArrow && 'nowrap'};
+ text-align: center;
+ font-size: ${props => (props.isWithArrow ? '20px' : '18px')};
+ text-decoration: none;
+ cursor: pointer;
+ outline: none;
+ transition: background-color 0.35s, border-color 0.35s;
+
+ // @todo Refactor to use theme props
+ ${props =>
+ props.bgColor === 'dark' &&
+ `
+ background-color: ${colors.brandDark};
+ color: ${colors.white};
+ `}
+
+ svg {
+ margin-left: 9px;
+ transition: transform 0.5s;
+ transform: translate3d(-2px, 2px, 0);
+ }
+
+ path {
+ fill: ${props => (props.isAccentColor ? props.theme.linkColor : props.color || props.theme.textColor)};
+ }
+
+ &:hover {
+ background-color: ${props => !props.isTransparent && !props.isWithArrow && '#04BEA8'};
+ border-color: ${props => props.isTransparent && !props.isNoBorder && !props.isWithArrow && '#00AE99'};
+
+ svg {
+ transform: translate3d(2px, -2px, 0);
+ }
+ }
+`;
diff --git a/packages/website/ts/components/chapter_link.tsx b/packages/website/ts/components/chapter_link.tsx
new file mode 100644
index 000000000..fd974cec1
--- /dev/null
+++ b/packages/website/ts/components/chapter_link.tsx
@@ -0,0 +1,15 @@
+import { NavLink as ReactRouterLink } from 'react-router-dom';
+import styled from 'styled-components';
+
+export const ChapterLink = styled(ReactRouterLink).attrs({
+ activeStyle: { opacity: 1 },
+})`
+ font-size: 1.222222222rem;
+ display: block;
+ opacity: 0.5;
+ margin-bottom: 1.666666667rem;
+
+ &:hover {
+ opacity: 1;
+ }
+`;
diff --git a/packages/website/ts/components/definition.tsx b/packages/website/ts/components/definition.tsx
new file mode 100644
index 000000000..8adef8d54
--- /dev/null
+++ b/packages/website/ts/components/definition.tsx
@@ -0,0 +1,145 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { Button } from 'ts/@next/components/button';
+import { Icon } from 'ts/@next/components/icon';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+
+interface Action {
+ label: string;
+ url?: string;
+ onClick?: () => void;
+ shouldUseAnchorTag?: boolean;
+}
+
+interface Props {
+ isInline?: boolean;
+ isInlineIcon?: boolean;
+ isCentered?: boolean;
+ isWithMargin?: boolean;
+ icon: string;
+ iconSize?: 'medium' | 'large' | number;
+ fontSize?: 'default' | 'medium' | number;
+ title: string;
+ titleSize?: 'small' | 'default' | number;
+ description: React.ReactNode | string;
+ actions?: Action[];
+}
+
+export const Definition = (props: Props) => (
+ <Wrap {...props}>
+ <Icon name={props.icon} size={props.iconSize || 'medium'} margin={[0, 0, 'default', 0]} />
+
+ <TextWrap {...props}>
+ <Heading
+ asElement="h2"
+ fontWeight="400"
+ marginBottom={props.titleSize === 'small' ? '7px' : '15px'}
+ size={props.titleSize || 'default'}
+ >
+ {props.title}
+ </Heading>
+
+ {typeof props.description === 'string' ? (
+ <Paragraph isMuted={true} size={props.fontSize || 'default'}>
+ {props.description}
+ </Paragraph>
+ ) : (
+ <>{props.description}</>
+ )}
+
+ {props.actions && (
+ <LinkWrap>
+ {props.actions.map((item, index) => (
+ <Button
+ key={`dlink-${index}`}
+ href={item.url}
+ onClick={item.onClick}
+ isWithArrow={true}
+ isAccentColor={true}
+ shouldUseAnchorTag={item.shouldUseAnchorTag}
+ target="_blank"
+ >
+ {item.label}
+ </Button>
+ ))}
+ </LinkWrap>
+ )}
+ </TextWrap>
+ </Wrap>
+);
+
+const Wrap =
+ styled.div <
+ Props >
+ `
+ max-width: ${props => props.isInline && '354px'};
+
+ & + & {
+ margin-top: ${props => props.isInlineIcon && '120px'};
+ margin-top: ${props => props.isWithMargin && '60px'};
+ }
+
+ @media (min-width: 768px) {
+ width: ${props => (props.isInline ? 'calc(33.3333% - 30px)' : '100%')};
+ display: ${props => props.isInlineIcon && 'flex'};
+ justify-content: ${props => props.isInlineIcon && 'space-between'};
+ align-items: ${props => props.isInlineIcon && 'center'};
+ text-align: ${props => (props.isInlineIcon || !props.isCentered) && 'left'};
+ }
+
+ @media (max-width: 768px) {
+ margin: 0 auto;
+
+ & + & {
+ margin-top: ${props => props.isInline && '60px'};
+ }
+ }
+`;
+
+const TextWrap =
+ styled.div <
+ Props >
+ `
+ width: 100%;
+ max-width: 560px;
+
+ ul {
+ padding-top: 10px;
+ padding-left: 1rem;
+ }
+
+ li {
+ color: ${props => props.theme.paragraphColor};
+ font-size: ${props => `var(--${props.fontSize || 'default'}Paragraph)`};
+ font-weight: 300;
+ list-style: disc;
+ opacity: 0.75;
+ line-height: 1.444444444;
+ margin-bottom: 1rem;
+ }
+
+ @media (min-width: 768px) {
+ margin-left: ${props => props.isInlineIcon && '60px'};
+ }
+`;
+
+const LinkWrap = styled.div`
+ margin-top: 60px;
+
+ @media (min-width: 768px) {
+ display: inline-flex;
+
+ a + a {
+ margin-left: 60px;
+ }
+ }
+
+ @media (max-width: 768px) {
+ max-width: 250px;
+
+ a + a {
+ margin-top: 15px;
+ }
+ }
+`;
diff --git a/packages/website/ts/components/dropdowns/dropdown_developers.tsx b/packages/website/ts/components/dropdowns/dropdown_developers.tsx
new file mode 100644
index 000000000..96d88846b
--- /dev/null
+++ b/packages/website/ts/components/dropdowns/dropdown_developers.tsx
@@ -0,0 +1,180 @@
+import { Link } from '@0x/react-shared';
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled, { withTheme } from 'styled-components';
+
+import { Button } from 'ts/@next/components/button';
+import { Column, FlexWrap, WrapGrid } from 'ts/@next/components/newLayout';
+import { ThemeValuesInterface } from 'ts/@next/components/siteWrap';
+import { Heading } from 'ts/@next/components/text';
+import { WebsitePaths } from 'ts/types';
+import { constants } from 'ts/utils/constants';
+
+interface Props {
+ theme: ThemeValuesInterface;
+}
+
+interface LinkConfig {
+ label: string;
+ url: string;
+ shouldOpenInNewTab?: boolean;
+}
+
+const introData: LinkConfig[] = [
+ {
+ label: 'Build a relayer',
+ url: `${WebsitePaths.Wiki}#Build-A-Relayer`,
+ },
+ {
+ label: 'Develop on Ethereum',
+ url: `${WebsitePaths.Wiki}#Ethereum-Development`,
+ },
+ {
+ label: 'Make & take orders',
+ url: `${WebsitePaths.Wiki}#Create,-Validate,-Fill-Order`,
+ },
+ {
+ label: 'Use networked liquidity',
+ url: `${WebsitePaths.Wiki}#Find,-Submit,-Fill-Order-From-Relayer`,
+ },
+];
+
+const docsData: LinkConfig[] = [
+ {
+ label: '0x.js',
+ url: WebsitePaths.ZeroExJs,
+ },
+ {
+ label: '0x Connect',
+ url: WebsitePaths.Connect,
+ },
+ {
+ label: 'Smart Contract',
+ url: WebsitePaths.SmartContracts,
+ },
+];
+
+const linksData: LinkConfig[] = [
+ {
+ label: 'Wiki',
+ url: WebsitePaths.Wiki,
+ },
+ {
+ label: 'Github',
+ url: constants.URL_GITHUB_ORG,
+ shouldOpenInNewTab: true,
+ },
+ {
+ label: 'Protocol specification',
+ url: constants.URL_PROTOCOL_SPECIFICATION,
+ shouldOpenInNewTab: true,
+ },
+];
+
+export const DropdownDevelopers: React.FunctionComponent<Props> = withTheme((props: Props) => (
+ <>
+ <DropdownWrap>
+ <div>
+ <Heading asElement="h4" size={14} color="inherit" marginBottom="15px" isMuted={0.35}>
+ Getting Started
+ </Heading>
+
+ <StyledGrid isCentered={false} isWrapped={true}>
+ {_.map(introData, (item, index) => (
+ <li>
+ <Link key={`introLink-${index}`} to={item.url}>
+ {item.label}
+ </Link>
+ </li>
+ ))}
+ </StyledGrid>
+ </div>
+
+ <StyledWrap>
+ <Column width="calc(100% - 15px)">
+ <Heading asElement="h4" size={14} color="inherit" marginBottom="15px" isMuted={0.35}>
+ Popular Docs
+ </Heading>
+
+ <ul>
+ {_.map(docsData, (item, index) => (
+ <li key={`docsLink-${index}`}>
+ <Link to={item.url} shouldOpenInNewTab={item.shouldOpenInNewTab}>
+ {item.label}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </Column>
+
+ <Column width="calc(100% - 15px)">
+ <Heading asElement="h4" size={14} color="inherit" marginBottom="15px" isMuted={0.35}>
+ Useful Links
+ </Heading>
+
+ <ul>
+ {_.map(linksData, (item, index) => (
+ <li key={`usefulLink-${index}`}>
+ <Link to={item.url} shouldOpenInNewTab={item.shouldOpenInNewTab}>
+ {item.label}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </Column>
+ </StyledWrap>
+
+ <StyledLink
+ to={WebsitePaths.Docs}
+ bgColor={props.theme.dropdownButtonBg}
+ isAccentColor={true}
+ isNoBorder={true}
+ >
+ View All Documentation
+ </StyledLink>
+ </DropdownWrap>
+ </>
+));
+
+const DropdownWrap = styled.div`
+ padding: 15px 30px 75px 30px;
+
+ a {
+ color: inherit;
+ }
+
+ li {
+ margin: 8px 0;
+ }
+`;
+
+const StyledGrid = styled(WrapGrid.withComponent('ul'))`
+ li {
+ width: 50%;
+ flex-shrink: 0;
+ }
+`;
+
+const StyledWrap = styled(FlexWrap)`
+ padding-top: 20px;
+ margin-top: 30px;
+ position: relative;
+
+ &:before {
+ content: '';
+ width: 100%;
+ height: 1px;
+ background-color: ${props => props.theme.dropdownColor};
+ opacity: 0.15;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+`;
+
+const StyledLink = styled(Button)`
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+`;
diff --git a/packages/website/ts/components/dropdowns/dropdown_products.tsx b/packages/website/ts/components/dropdowns/dropdown_products.tsx
new file mode 100644
index 000000000..886cee44a
--- /dev/null
+++ b/packages/website/ts/components/dropdowns/dropdown_products.tsx
@@ -0,0 +1,48 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+import { WebsitePaths } from 'ts/types';
+
+const navData = [
+ {
+ title: '0x Instant',
+ description: 'Simple crypto purchasing',
+ url: WebsitePaths.Instant,
+ },
+ {
+ title: '0x Launch Kit',
+ description: 'Build on the 0x protocol',
+ url: WebsitePaths.LaunchKit,
+ },
+];
+
+export const DropdownProducts: React.FunctionComponent<{}> = () => (
+ <List>
+ {_.map(navData, (item, index) => (
+ <li key={`productLink-${index}`}>
+ <Link to={item.url}>
+ <Heading asElement="h3" color="inherit" isNoMargin={true} size="small">
+ {item.title}
+ </Heading>
+
+ {item.description && (
+ <Paragraph color="inherit" isNoMargin={true} size="small" isMuted={0.5}>
+ {item.description}
+ </Paragraph>
+ )}
+ </Link>
+ </li>
+ ))}
+ </List>
+);
+
+const List = styled.ul`
+ a {
+ padding: 15px 30px;
+ display: block;
+ color: inherit;
+ }
+`;
diff --git a/packages/website/ts/components/footer.tsx b/packages/website/ts/components/footer.tsx
index 6366bf4ea..b30a0cc5e 100644
--- a/packages/website/ts/components/footer.tsx
+++ b/packages/website/ts/components/footer.tsx
@@ -1,228 +1,168 @@
-import { ALink, colors, Link } from '@0x/react-shared';
-import { ObjectMap } from '@0x/types';
+import { Link as SmartLink } from '@0x/react-shared';
import * as _ from 'lodash';
-import DropDownMenu from 'material-ui/DropDownMenu';
-import MenuItem from 'material-ui/MenuItem';
import * as React from 'react';
+import MediaQuery from 'react-responsive';
+import styled from 'styled-components';
-import { Dispatcher } from 'ts/redux/dispatcher';
-import { Deco, Key, Language, WebsitePaths } from 'ts/types';
+import { Logo } from 'ts/@next/components/logo';
+import { Column, FlexWrap, WrapGrid } from 'ts/@next/components/newLayout';
+import { NewsletterForm } from 'ts/@next/components/newsletter_form';
+import { WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
-import { Translate } from 'ts/utils/translate';
-
-const ICON_DIMENSION = 16;
-
-const languageToMenuTitle = {
- [Language.English]: 'English',
- [Language.Russian]: 'Русский',
- [Language.Spanish]: 'Español',
- [Language.Korean]: '한국어',
- [Language.Chinese]: '中文',
-};
-
-export interface FooterProps {
- translate: Translate;
- dispatcher: Dispatcher;
- backgroundColor?: string;
+
+interface LinkInterface {
+ text: string;
+ url: string;
+ shouldOpenInNewTab?: boolean;
+}
+
+interface LinkRows {
+ heading: string;
+ isOnMobile?: boolean;
+ links: LinkInterface[];
}
-interface FooterState {
- selectedLanguage: Language;
+interface LinkListProps {
+ links: LinkInterface[];
}
-export class Footer extends React.Component<FooterProps, FooterState> {
- public static defaultProps = {
- backgroundColor: colors.darkerGrey,
- };
- constructor(props: FooterProps) {
- super(props);
- this.state = {
- selectedLanguage: props.translate.getLanguage(),
- };
+const linkRows: LinkRows[] = [
+ {
+ heading: 'Products',
+ isOnMobile: true,
+ links: [
+ { url: WebsitePaths.Instant, text: '0x Instant' },
+ { url: WebsitePaths.LaunchKit, text: '0x Launch Kit' },
+ ],
+ },
+ {
+ heading: 'Developers',
+ links: [
+ { url: WebsitePaths.Docs, text: 'Documentation' },
+ { url: constants.URL_GITHUB_ORG, text: 'GitHub', shouldOpenInNewTab: true },
+ { url: constants.URL_PROTOCOL_SPECIFICATION, text: 'Protocol Spec', shouldOpenInNewTab: true },
+ ],
+ },
+ {
+ heading: 'About',
+ isOnMobile: true,
+ links: [
+ { url: WebsitePaths.AboutMission, text: 'Mission' },
+ { url: WebsitePaths.AboutTeam, text: 'Team' },
+ { url: WebsitePaths.AboutJobs, text: 'Jobs' },
+ { url: WebsitePaths.AboutPress, text: 'Press' },
+ { url: WebsitePaths.Ecosystem, text: 'Grant Program' },
+ ],
+ },
+ {
+ heading: 'Community',
+ isOnMobile: true,
+ links: [
+ { url: constants.URL_TWITTER, text: 'Twitter', shouldOpenInNewTab: true },
+ { url: constants.URL_ZEROEX_CHAT, text: 'Discord Chat', shouldOpenInNewTab: true },
+ { url: constants.URL_FACEBOOK, text: 'Facebook', shouldOpenInNewTab: true },
+ { url: constants.URL_REDDIT, text: 'Reddit', shouldOpenInNewTab: true },
+ ],
+ },
+];
+
+export const Footer: React.StatelessComponent = () => (
+ <FooterWrap>
+ <FlexWrap>
+ <FooterColumn width="35%">
+ <Logo />
+ <NewsletterForm />
+ </FooterColumn>
+
+ <FooterColumn width="55%">
+ <WrapGrid isCentered={false} isWrapped={true}>
+ {_.map(linkRows, (row: LinkRows, index) => (
+ <MediaQuery minWidth={row.isOnMobile ? 0 : 768} key={`fc-${index}`}>
+ <FooterSectionWrap>
+ <RowHeading>{row.heading}</RowHeading>
+
+ <LinkList links={row.links} />
+ </FooterSectionWrap>
+ </MediaQuery>
+ ))}
+ </WrapGrid>
+ </FooterColumn>
+ </FlexWrap>
+ </FooterWrap>
+);
+
+const LinkList = (props: LinkListProps) => (
+ <List>
+ {_.map(props.links, (link, index) => (
+ <li key={`fl-${index}`}>
+ <Link to={link.url} shouldOpenInNewTab={link.shouldOpenInNewTab}>
+ {link.text}
+ </Link>
+ </li>
+ ))}
+ </List>
+);
+
+const FooterWrap = styled.footer`
+ padding: 40px 30px 30px 30px;
+ margin-top: 30px;
+ background-color: ${props => props.theme.footerBg};
+ color: ${props => props.theme.footerColor};
+
+ path {
+ fill: ${props => props.theme.footerColor};
+ }
+
+ @media (min-width: 768px) {
+ height: 350px;
}
- public render(): React.ReactNode {
- const sectionNameToLinks: ObjectMap<ALink[]> = {
- [Key.Documentation]: [
- {
- title: 'Developer Home',
- to: WebsitePaths.Docs,
- },
- {
- title: '0x.js',
- to: WebsitePaths.ZeroExJs,
- },
- {
- title: this.props.translate.get(Key.SmartContracts, Deco.Cap),
- to: WebsitePaths.SmartContracts,
- },
- {
- title: this.props.translate.get(Key.Connect, Deco.Cap),
- to: WebsitePaths.Connect,
- },
- {
- title: this.props.translate.get(Key.Whitepaper, Deco.Cap),
- to: WebsitePaths.Whitepaper,
- shouldOpenInNewTab: true,
- },
- {
- title: this.props.translate.get(Key.Wiki, Deco.Cap),
- to: WebsitePaths.Wiki,
- },
- ],
- [Key.Community]: [
- {
- title: this.props.translate.get(Key.Discord, Deco.Cap),
- to: constants.URL_ZEROEX_CHAT,
- shouldOpenInNewTab: true,
- },
- {
- title: this.props.translate.get(Key.Blog, Deco.Cap),
- to: constants.URL_BLOG,
- shouldOpenInNewTab: true,
- },
- {
- title: 'Twitter',
- to: constants.URL_TWITTER,
- shouldOpenInNewTab: true,
- },
- {
- title: 'Reddit',
- to: constants.URL_REDDIT,
- shouldOpenInNewTab: true,
- },
- {
- title: this.props.translate.get(Key.Forum, Deco.Cap),
- to: constants.URL_DISCOURSE_FORUM,
- shouldOpenInNewTab: true,
- },
- ],
- [Key.Organization]: [
- {
- title: this.props.translate.get(Key.About, Deco.Cap),
- to: WebsitePaths.About,
- },
- {
- title: this.props.translate.get(Key.Careers, Deco.Cap),
- to: WebsitePaths.Careers,
- },
- {
- title: this.props.translate.get(Key.Contact, Deco.Cap),
- to: 'mailto:team@0x.org',
- shouldOpenInNewTab: true,
- },
- ],
- };
- const languageMenuItems = _.map(languageToMenuTitle, (menuTitle: string, language: Language) => {
- return <MenuItem key={menuTitle} value={language} primaryText={menuTitle} />;
- });
- return (
- <div className="relative pb4 pt2" style={{ backgroundColor: this.props.backgroundColor }}>
- <div className="mx-auto max-width-4 md-px2 lg-px0 py4 clearfix" style={{ color: colors.white }}>
- <div className="col lg-col-4 md-col-4 col-12 left">
- <div className="sm-mx-auto" style={{ width: 148 }}>
- <div>
- <img src="/images/protocol_logo_white.png" height="30" />
- </div>
- <div
- style={{
- fontSize: 11,
- color: colors.grey,
- paddingLeft: 37,
- paddingTop: 2,
- }}
- >
- © ZeroEx, Intl.
- </div>
- <div className="pt4 center">
- <DropDownMenu
- labelStyle={{ color: colors.white }}
- value={this.state.selectedLanguage}
- onChange={this._updateLanguage.bind(this)}
- >
- {languageMenuItems}
- </DropDownMenu>
- </div>
- </div>
- </div>
- <div className="col lg-col-8 md-col-8 col-12 lg-pl4 md-pl4">
- <div className="col lg-col-4 md-col-4 col-12">
- <div className="lg-right md-right sm-center">
- {this._renderHeader(Key.Documentation)}
- {_.map(sectionNameToLinks[Key.Documentation], this._renderMenuItem.bind(this))}
- </div>
- </div>
- <div className="col lg-col-4 md-col-4 col-12 lg-pr2 md-pr2">
- <div className="lg-right md-right sm-center">
- {this._renderHeader(Key.Community)}
- {_.map(sectionNameToLinks[Key.Community], this._renderMenuItem.bind(this))}
- </div>
- </div>
- <div className="col lg-col-4 md-col-4 col-12">
- <div className="lg-right md-right sm-center">
- {this._renderHeader(Key.Organization)}
- {_.map(sectionNameToLinks[Key.Organization], this._renderMenuItem.bind(this))}
- </div>
- </div>
- </div>
- </div>
- </div>
- );
+`;
+
+const FooterColumn = styled(Column)`
+ @media (min-width: 768px) {
+ width: ${props => props.width};
}
- private _renderIcon(fileName: string): React.ReactNode {
- return (
- <div style={{ height: ICON_DIMENSION, width: ICON_DIMENSION }}>
- <img src={`/images/social/${fileName}`} style={{ width: ICON_DIMENSION }} />
- </div>
- );
+
+ @media (max-width: 768px) {
+ text-align: left;
}
- private _renderMenuItem(link: ALink): React.ReactNode {
- const titleToIcon: { [title: string]: string } = {
- [this.props.translate.get(Key.Discord, Deco.Cap)]: 'discord.png',
- [this.props.translate.get(Key.Blog, Deco.Cap)]: 'medium.png',
- Twitter: 'twitter.png',
- Reddit: 'reddit.png',
- [this.props.translate.get(Key.Forum, Deco.Cap)]: 'discourse.png',
- };
- const iconIfExists = titleToIcon[link.title];
- return (
- <div key={link.title} className="sm-center" style={{ fontSize: 13, paddingTop: 25 }}>
- <Link
- to={link.to}
- shouldOpenInNewTab={link.shouldOpenInNewTab}
- fontColor={colors.white}
- className="text-decoration-none"
- >
- <div>
- {!_.isUndefined(iconIfExists) ? (
- <div className="inline-block">
- <div className="pr1 table-cell">{this._renderIcon(iconIfExists)}</div>
- <div className="table-cell">{link.title}</div>
- </div>
- ) : (
- link.title
- )}
- </div>
- </Link>
- </div>
- );
+`;
+
+const FooterSectionWrap = styled(FooterColumn)`
+ @media (max-width: 768px) {
+ width: 50%;
+
+ & + & {
+ margin-top: 0;
+ margin-bottom: 30px;
+ }
}
- private _renderHeader(key: Key): React.ReactNode {
- const headerStyle = {
- color: colors.grey400,
- letterSpacing: 2,
- fontFamily: 'Roboto Mono',
- fontSize: 13,
- };
- return (
- <div className="lg-pb2 md-pb2 sm-pt4" style={headerStyle}>
- {this.props.translate.get(key, Deco.Upper)}
- </div>
- );
+`;
+
+const RowHeading = styled.h3`
+ color: inherit;
+ font-weight: 700;
+ font-size: 16px;
+ margin-bottom: 1.25em;
+ opacity: 0.75;
+`;
+
+const List = styled.ul`
+ li + li {
+ margin-top: 8px;
}
- private _updateLanguage(_event: any, _index: number, value: Language): void {
- this.setState({
- selectedLanguage: value,
- });
- this.props.dispatcher.updateSelectedLanguage(value);
+`;
+
+const Link = styled(SmartLink)`
+ color: inherit;
+ opacity: 0.5;
+ display: block;
+ font-size: 16px;
+ line-height: 20px;
+ transition: opacity 0.25s;
+ text-decoration: none;
+
+ &:hover {
+ opacity: 0.8;
}
-}
+`;
diff --git a/packages/website/ts/components/hamburger.tsx b/packages/website/ts/components/hamburger.tsx
new file mode 100644
index 000000000..435d206cd
--- /dev/null
+++ b/packages/website/ts/components/hamburger.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+interface Props {
+ isOpen: boolean;
+ onClick?: () => void;
+}
+
+export const Hamburger: React.FunctionComponent<Props> = (props: Props) => {
+ return (
+ <StyledHamburger isOpen={props.isOpen} onClick={props.onClick}>
+ <span />
+ <span />
+ <span />
+ </StyledHamburger>
+ );
+};
+
+const StyledHamburger =
+ styled.button <
+ Props >
+ `
+ background: none;
+ border: 0;
+ width: 22px;
+ height: 16px;
+ position: relative;
+ z-index: 25;
+ padding: 0;
+ outline: none;
+ user-select: none;
+
+ @media (min-width: 800px) {
+ display: none;
+ }
+
+ span {
+ display: block;
+ background-color: ${props => props.theme.textColor};
+ width: 100%;
+ height: 2px;
+ margin-bottom: 5px;
+ transform-origin: 4px 0px;
+ transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ background-color 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ opacity 0.55s ease;
+
+ &:first-child {
+ //transform-origin: 0% 0%;
+ }
+
+ &:last-child {
+ //transform-origin: 0% 100%;
+ }
+
+ ${props =>
+ props.isOpen &&
+ `
+ opacity: 1;
+ transform: rotate(45deg) translate(0, 1px);
+
+ &:nth-child(2) {
+ opacity: 0;
+ transform: rotate(0deg) scale(0.2, 0.2);
+ }
+
+ &:last-child {
+ transform: rotate(-45deg) translate(1px, -4px);
+ }
+ `}
+ }
+`;
diff --git a/packages/website/ts/components/header.tsx b/packages/website/ts/components/header.tsx
new file mode 100644
index 000000000..e886923df
--- /dev/null
+++ b/packages/website/ts/components/header.tsx
@@ -0,0 +1,252 @@
+import { Link } from '@0x/react-shared';
+import _ from 'lodash';
+import * as React from 'react';
+import MediaQuery from 'react-responsive';
+import styled, { css, withTheme } from 'styled-components';
+
+import Headroom from 'react-headroom';
+
+import { Button } from 'ts/@next/components/button';
+import { DropdownDevelopers } from 'ts/@next/components/dropdowns/dropdown_developers';
+import { DropdownProducts } from 'ts/@next/components/dropdowns/dropdown_products';
+import { Hamburger } from 'ts/@next/components/hamburger';
+import { Logo } from 'ts/@next/components/logo';
+import { MobileNav } from 'ts/@next/components/mobileNav';
+import { FlexWrap } from 'ts/@next/components/newLayout';
+import { ThemeValuesInterface } from 'ts/@next/components/siteWrap';
+import { WebsitePaths } from 'ts/types';
+import { constants } from 'ts/utils/constants';
+
+interface HeaderProps {
+ location?: Location;
+ isNavToggled?: boolean;
+ toggleMobileNav?: () => void;
+ theme: ThemeValuesInterface;
+}
+
+interface NavItemProps {
+ url?: string;
+ id?: string;
+ text?: string;
+ dropdownWidth?: number;
+ dropdownComponent?: React.FunctionComponent<any>;
+ shouldOpenInNewTab?: boolean;
+}
+
+interface DropdownWrapInterface {
+ width?: number;
+}
+
+const navItems: NavItemProps[] = [
+ {
+ id: 'why',
+ url: WebsitePaths.Why,
+ text: 'Why 0x',
+ },
+ {
+ id: 'products',
+ text: 'Products',
+ dropdownComponent: DropdownProducts,
+ dropdownWidth: 280,
+ },
+ {
+ id: 'developers',
+ text: 'Developers',
+ dropdownComponent: DropdownDevelopers,
+ dropdownWidth: 480,
+ },
+ {
+ id: 'about',
+ url: WebsitePaths.AboutMission,
+ text: 'About',
+ },
+ {
+ id: 'blog',
+ url: constants.URL_BLOG,
+ shouldOpenInNewTab: true,
+ text: 'Blog',
+ },
+];
+
+class HeaderBase extends React.Component<HeaderProps> {
+ public onUnpin = () => {
+ if (this.props.isNavToggled) {
+ this.props.toggleMobileNav();
+ }
+ };
+
+ public render(): React.ReactNode {
+ const { isNavToggled, toggleMobileNav, theme } = this.props;
+
+ return (
+ <Headroom onUnpin={this.onUnpin} downTolerance={4} upTolerance={10}>
+ <StyledHeader isNavToggled={isNavToggled}>
+ <HeaderWrap>
+ <Link to={WebsitePaths.Home}>
+ <Logo />
+ </Link>
+
+ <NavLinks>
+ {_.map(navItems, (link, index) => <NavItem key={`navlink-${index}`} link={link} />)}
+ </NavLinks>
+
+ <MediaQuery minWidth={990}>
+ <TradeButton bgColor={theme.headerButtonBg} color="#ffffff" href="/portal">
+ Trade on 0x
+ </TradeButton>
+ </MediaQuery>
+
+ <Hamburger isOpen={isNavToggled} onClick={toggleMobileNav} />
+ <MobileNav isToggled={isNavToggled} toggleMobileNav={toggleMobileNav} />
+ </HeaderWrap>
+ </StyledHeader>
+ </Headroom>
+ );
+ }
+}
+
+export const Header = withTheme(HeaderBase);
+
+const NavItem = (props: { link: NavItemProps; key: string }) => {
+ const { link } = props;
+ const Subnav = link.dropdownComponent;
+ const linkElement = _.isUndefined(link.url) ? (
+ <StyledAnchor href="#">{link.text}</StyledAnchor>
+ ) : (
+ <StyledNavLink to={link.url} shouldOpenInNewTab={link.shouldOpenInNewTab}>
+ {link.text}
+ </StyledNavLink>
+ );
+ return (
+ <LinkWrap>
+ {linkElement}
+
+ {link.dropdownComponent && (
+ <DropdownWrap width={link.dropdownWidth}>
+ <Subnav />
+ </DropdownWrap>
+ )}
+ </LinkWrap>
+ );
+};
+
+const StyledHeader =
+ styled.header <
+ HeaderProps >
+ `
+ padding: 30px;
+ background-color: ${props => props.theme.bgColor};
+`;
+
+const LinkWrap = styled.li`
+ position: relative;
+
+ a {
+ display: block;
+ }
+
+ @media (min-width: 800px) {
+ &:hover > div {
+ display: block;
+ visibility: visible;
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ transition: opacity 0.35s, transform 0.35s, visibility 0s;
+ }
+ }
+`;
+
+const linkStyles = css`
+ color: ${props => props.theme.textColor};
+ opacity: 0.5;
+ transition: opacity 0.35s;
+ padding: 15px 0;
+ margin: 0 30px;
+
+ &:hover {
+ opacity: 1;
+ }
+`;
+
+const StyledNavLink = styled(Link).attrs({
+ activeStyle: { opacity: 1 },
+})`
+ ${linkStyles};
+`;
+
+const StyledAnchor = styled.a`
+ ${linkStyles};
+ cursor: default;
+`;
+
+const HeaderWrap = styled(FlexWrap)`
+ justify-content: space-between;
+ align-items: center;
+
+ @media (max-width: 800px) {
+ padding-top: 0;
+ display: flex;
+ padding-bottom: 0;
+ }
+`;
+
+const NavLinks = styled.ul`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ @media (max-width: 800px) {
+ display: none;
+ }
+`;
+
+const DropdownWrap =
+ styled.div <
+ DropdownWrapInterface >
+ `
+ width: ${props => props.width || 280}px;
+ padding: 15px 0;
+ border: 1px solid transparent;
+ border-color: ${props => props.theme.dropdownBorderColor};
+ background-color: ${props => props.theme.dropdownBg};
+ color: ${props => props.theme.dropdownColor};
+ position: absolute;
+ top: 100%;
+ left: calc(50% - ${props => (props.width || 280) / 2}px);
+ visibility: hidden;
+ opacity: 0;
+ transform: translate3d(0, -10px, 0);
+ transition: opacity 0.35s, transform 0.35s, visibility 0s 0.35s;
+ z-index: 20;
+
+ &:after, &:before {
+ bottom: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+ &:after {
+ border-color: rgba(255, 255, 255, 0);
+ border-bottom-color: ${props => props.theme.dropdownBg};
+ border-width: 10px;
+ margin-left: -10px;
+ }
+ &:before {
+ border-color: rgba(255, 0, 0, 0);
+ border-bottom-color: ${props => props.theme.dropdownBorderColor};
+ border-width: 11px;
+ margin-left: -11px;
+ }
+
+ @media (max-width: 768px) {
+ display: none;
+ }
+`;
+
+const TradeButton = styled(Button)`
+ padding: 14px 22px !important;
+`;
diff --git a/packages/website/ts/components/hero.tsx b/packages/website/ts/components/hero.tsx
new file mode 100644
index 000000000..4c8874d3e
--- /dev/null
+++ b/packages/website/ts/components/hero.tsx
@@ -0,0 +1,152 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { addFadeInAnimation } from 'ts/@next/constants/animations';
+
+interface Props {
+ title: string;
+ maxWidth?: string;
+ maxWidthHeading?: string;
+ isLargeTitle?: boolean;
+ isFullWidth?: boolean;
+ isCenteredMobile?: boolean;
+ description: string;
+ figure?: React.ReactNode;
+ actions?: React.ReactNode;
+}
+
+const Section = styled.section`
+ padding: 120px 0;
+
+ @media (max-width: 768px) {
+ padding: 60px 0;
+ }
+`;
+
+interface WrapProps {
+ isCentered?: boolean;
+ isFullWidth?: boolean;
+ isCenteredMobile?: boolean;
+}
+const Wrap =
+ styled.div <
+ WrapProps >
+ `
+ width: calc(100% - 60px);
+ margin: 0 auto;
+
+ @media (min-width: 768px) {
+ max-width: ${props => (!props.isFullWidth ? '895px' : '1136px')};
+ flex-direction: row-reverse;
+ display: flex;
+ align-items: center;
+ text-align: ${props => props.isCentered && 'center'};
+ justify-content: ${props => (props.isCentered ? 'center' : 'space-between')};
+ }
+
+ @media (max-width: 768px) {
+ text-align: ${props => (props.isCenteredMobile ? `center` : 'left')};
+ }
+`;
+
+interface TitleProps {
+ isLarge?: any;
+ maxWidth?: string;
+}
+const Title =
+ styled.h1 <
+ TitleProps >
+ `
+ font-size: ${props => (props.isLarge ? '80px' : '50px')};
+ font-weight: 300;
+ line-height: 1.1;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 30px;
+ max-width: ${props => props.maxWidth};
+ ${addFadeInAnimation('0.5s')}
+
+ @media (max-width: 1024px) {
+ font-size: 60px;
+ }
+
+ @media (max-width: 768px) {
+ font-size: 46px;
+ }
+`;
+
+const Description = styled.p`
+ font-size: 22px;
+ line-height: 31px;
+ font-weight: 300;
+ padding: 0;
+ margin-bottom: 50px;
+ color: ${props => props.theme.introTextColor};
+ ${addFadeInAnimation('0.5s', '0.15s')} @media (max-width: 1024px) {
+ margin-bottom: 30px;
+ }
+`;
+
+const Content =
+ styled.div <
+ { width: string } >
+ `
+ width: 100%;
+
+ @media (min-width: 768px) {
+ max-width: ${props => props.width};
+ }
+`;
+
+const ButtonWrap = styled.div`
+ display: inline-flex;
+ align-items: center;
+
+ * + * {
+ margin-left: 12px;
+ }
+
+ > *:nth-child(1) {
+ ${addFadeInAnimation('0.6s', '0.3s')};
+ }
+ > *:nth-child(2) {
+ ${addFadeInAnimation('0.6s', '0.4s')};
+ }
+
+ @media (max-width: 500px) {
+ flex-direction: column;
+ justify-content: center;
+
+ * {
+ padding-left: 20px;
+ padding-right: 20px;
+ }
+
+ * + * {
+ margin-left: 0;
+ margin-top: 12px;
+ }
+ }
+`;
+
+export const Hero: React.StatelessComponent<Props> = (props: Props) => (
+ <Section>
+ <Wrap isCentered={!props.figure} isFullWidth={props.isFullWidth} isCenteredMobile={props.isCenteredMobile}>
+ {props.figure && <Content width="400px">{props.figure}</Content>}
+
+ <Content width={props.maxWidth ? props.maxWidth : props.figure ? '546px' : '678px'}>
+ <Title isLarge={props.isLargeTitle} maxWidth={props.maxWidthHeading}>
+ {props.title}
+ </Title>
+
+ <Description>{props.description}</Description>
+
+ {props.actions && <ButtonWrap>{props.actions}</ButtonWrap>}
+ </Content>
+ </Wrap>
+ </Section>
+);
+
+Hero.defaultProps = {
+ isCenteredMobile: true,
+};
diff --git a/packages/website/ts/components/heroAnimation.tsx b/packages/website/ts/components/heroAnimation.tsx
new file mode 100644
index 000000000..42956fb6a
--- /dev/null
+++ b/packages/website/ts/components/heroAnimation.tsx
@@ -0,0 +1,123 @@
+import * as React from 'react';
+import styled, { keyframes } from 'styled-components';
+
+export const HeroAnimation = () => (
+ <Image width="404" height="404" viewBox="0 0 404 404" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="404" height="404">
+ <circle cx="202" cy="202" r="200" fill="#00AE99" stroke="#00AE99" stroke-width="3" />
+ </mask>
+ <g mask="url(#mask0)">
+ <circle cx="202" cy="202" r="200" stroke="#00AE99" strokeWidth="3" />
+ <TopCircle
+ vector-effect="non-scaling-stroke"
+ cx="201.667"
+ cy="68.6667"
+ r="66.6667"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ <LeftCircle
+ vector-effect="non-scaling-stroke"
+ cx="68.6667"
+ cy="202.667"
+ r="66.6667"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ <Logo
+ vector-effect="non-scaling-stroke"
+ d="M168.17 260.29L167.271 259.089L165.46 260.444L167.413 261.585L168.17 260.29ZM197.32 269.2L197.219 270.696L197.226 270.697L197.32 269.2ZM237.414 258.856L238.22 260.12L238.225 260.117L237.414 258.856ZM252.653 245.439L253.801 246.405L254.55 245.515L253.874 244.568L252.653 245.439ZM241.096 229.872L242.285 228.958L242.281 228.952L242.276 228.946L241.096 229.872ZM237.72 225.571L238.901 224.645L237.582 222.965L236.449 224.775L237.72 225.571ZM219.719 241.445L218.672 242.519L219.418 243.246L220.36 242.801L219.719 241.445ZM208.264 230.282L209.311 229.207L208.392 228.312L207.365 229.081L208.264 230.282ZM143.827 169.549L145.02 168.64L143.647 166.838L142.524 168.806L143.827 169.549ZM135.133 198.43L133.637 198.329L133.636 198.337L135.133 198.43ZM145.464 238.577L144.201 239.388L145.464 238.577ZM158.862 253.837L157.895 254.984L158.786 255.736L159.735 255.057L158.862 253.837ZM174.409 242.264L175.324 243.453L175.33 243.448L175.336 243.443L174.409 242.264ZM178.705 238.885L179.632 240.064L181.287 238.761L179.516 237.623L178.705 238.885ZM162.851 220.757L161.78 219.707L161.049 220.452L161.495 221.397L162.851 220.757ZM174.102 209.286L175.173 210.337L176.082 209.41L175.295 208.377L174.102 209.286ZM235.163 145.072L236.036 146.292L237.92 144.945L235.92 143.777L235.163 145.072ZM206.014 136.162L205.91 137.658L205.913 137.658L206.014 136.162ZM165.817 146.506L166.629 147.767L166.632 147.765L165.817 146.506ZM150.578 159.922L149.43 158.956L148.681 159.846L149.357 160.793L150.578 159.922ZM162.135 175.489L160.946 176.403L160.951 176.409L160.955 176.415L162.135 175.489ZM165.511 179.791L164.331 180.717L165.634 182.378L166.773 180.6L165.511 179.791ZM183.614 163.916L184.655 162.836L183.913 162.122L182.98 162.557L183.614 163.916ZM194.354 174.26L193.313 175.341L194.212 176.206L195.226 175.48L194.354 174.26ZM259.608 235.505L258.411 236.409L259.789 238.233L260.914 236.243L259.608 235.505ZM268.2 206.931L269.696 207.033L269.697 207.024L268.2 206.931ZM257.87 166.784L259.135 165.979L259.132 165.974L257.87 166.784ZM244.471 151.524L245.439 150.378L244.547 149.625L243.598 150.304L244.471 151.524ZM228.924 163.097L228.009 161.909L228.003 161.913L227.997 161.918L228.924 163.097ZM224.629 166.477L223.701 165.298L222.034 166.609L223.826 167.744L224.629 166.477ZM240.584 184.604L239.228 185.244L239.235 185.259L239.242 185.274L240.584 184.604ZM240.687 184.809L241.767 185.849L242.502 185.086L242.029 184.139L240.687 184.809ZM229.845 196.075L228.764 195.035L227.877 195.957L228.648 196.979L229.845 196.075ZM167.413 261.585C176.201 266.718 186.346 269.964 197.219 270.696L197.421 267.703C187.019 267.002 177.321 263.898 168.926 258.994L167.413 261.585ZM197.226 270.697C212.283 271.639 226.405 267.659 238.22 260.12L236.607 257.591C225.307 264.8 211.813 268.604 197.413 267.703L197.226 270.697ZM238.225 260.117C244.08 256.348 249.307 251.742 253.801 246.405L251.506 244.473C247.204 249.583 242.203 253.989 236.602 257.594L238.225 260.117ZM253.874 244.568C250.283 239.533 246.385 234.295 242.285 228.958L239.906 230.786C243.989 236.1 247.864 241.309 251.432 246.31L253.874 244.568ZM242.276 228.946C241.713 228.229 241.151 227.512 240.588 226.795C240.026 226.078 239.463 225.362 238.901 224.645L236.54 226.497C237.103 227.213 237.665 227.93 238.228 228.647C238.791 229.364 239.353 230.081 239.916 230.798L242.276 228.946ZM236.449 224.775C232.311 231.384 226.193 236.725 219.078 240.089L220.36 242.801C227.974 239.201 234.538 233.481 238.992 226.367L236.449 224.775ZM220.766 240.371L209.311 229.207L207.217 231.356L218.672 242.519L220.766 240.371ZM207.365 229.081L167.271 259.089L169.069 261.49L209.163 231.483L207.365 229.081ZM142.524 168.806C137.505 177.601 134.368 187.549 133.637 198.329L136.63 198.532C137.33 188.214 140.33 178.703 145.13 170.293L142.524 168.806ZM133.636 198.337C132.696 213.409 136.668 227.654 144.201 239.388L146.726 237.767C139.531 226.56 135.73 212.947 136.63 198.524L133.636 198.337ZM144.201 239.388C147.965 245.25 152.565 250.484 157.895 254.984L159.83 252.691C154.727 248.383 150.327 243.376 146.726 237.767L144.201 239.388ZM159.735 255.057C164.764 251.461 169.994 247.558 175.324 243.453L173.494 241.076C168.187 245.164 162.985 249.045 157.99 252.617L159.735 255.057ZM175.336 243.443C176.768 242.317 178.2 241.19 179.632 240.064L177.777 237.706C176.345 238.832 174.913 239.959 173.481 241.086L175.336 243.443ZM179.516 237.623C172.904 233.374 167.568 227.241 164.208 220.117L161.495 221.397C165.09 229.021 170.8 235.588 177.894 240.147L179.516 237.623ZM163.922 221.807L175.173 210.337L173.031 208.236L161.78 219.707L163.922 221.807ZM175.295 208.377L145.02 168.64L142.634 170.458L172.909 210.196L175.295 208.377ZM235.92 143.777C227.132 138.643 216.987 135.398 206.114 134.665L205.913 137.658C216.315 138.359 226.012 141.463 234.407 146.367L235.92 143.777ZM206.118 134.665C191.055 133.618 176.824 137.599 165.003 145.246L166.632 147.765C177.926 140.459 191.515 136.657 205.91 137.658L206.118 134.665ZM165.006 145.244C159.151 149.013 153.924 153.619 149.43 158.956L151.725 160.888C156.027 155.779 161.028 151.372 166.629 147.767L165.006 145.244ZM149.357 160.793C152.948 165.828 156.846 171.066 160.946 176.403L163.325 174.575C159.242 169.261 155.367 164.052 151.799 159.051L149.357 160.793ZM160.955 176.415C161.518 177.132 162.08 177.849 162.643 178.566C163.205 179.283 163.768 180 164.331 180.717L166.691 178.865C166.128 178.148 165.566 177.431 165.003 176.714C164.441 175.997 163.878 175.28 163.315 174.563L160.955 176.415ZM166.773 180.6C171.021 173.973 177.044 168.635 184.248 165.276L182.98 162.557C175.251 166.161 168.796 171.885 164.248 178.981L166.773 180.6ZM182.574 164.997L193.313 175.341L195.394 173.18L184.655 162.836L182.574 164.997ZM195.226 175.48L236.036 146.292L234.291 143.852L193.481 173.04L195.226 175.48ZM260.914 236.243C265.827 227.556 268.964 217.713 269.696 207.033L266.703 206.828C266.003 217.042 263.004 226.453 258.303 234.767L260.914 236.243ZM269.697 207.024C270.638 191.949 266.663 177.81 259.135 165.979L256.604 167.589C263.804 178.904 267.603 192.417 266.703 206.837L269.697 207.024ZM259.132 165.974C255.368 160.111 250.769 154.878 245.439 150.378L243.503 152.67C248.606 156.978 253.007 161.986 256.607 167.594L259.132 165.974ZM243.598 150.304C238.57 153.901 233.339 157.803 228.009 161.909L229.84 164.285C235.147 160.197 240.349 156.316 245.344 152.744L243.598 150.304ZM227.997 161.918C227.281 162.481 226.565 163.045 225.849 163.608C225.133 164.171 224.417 164.734 223.701 165.298L225.556 167.656C226.272 167.092 226.988 166.529 227.704 165.966C228.42 165.402 229.136 164.839 229.852 164.276L227.997 161.918ZM223.826 167.744C230.535 171.992 235.869 178.121 239.228 185.244L241.941 183.964C238.345 176.339 232.632 169.769 225.431 165.209L223.826 167.744ZM239.242 185.274L239.345 185.479L242.029 184.139L241.926 183.934L239.242 185.274ZM239.606 183.769L228.764 195.035L230.926 197.115L241.767 185.849L239.606 183.769ZM228.648 196.979L258.411 236.409L260.806 234.601L231.042 195.171L228.648 196.979Z"
+ fill="#00AE99"
+ />
+ <Rectangle
+ vector-effect="non-scaling-stroke"
+ d="M269 135V268.333H442V135H269Z"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ <Square
+ vector-effect="non-scaling-stroke"
+ d="M339.64 269.64L270 339.281L343.913 413.194L413.554 343.554L339.64 269.64Z"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ <Oblong
+ vector-effect="non-scaling-stroke"
+ d="M202.5 269C202.5 269 269 269 269 335.5C269 402 202.5 402 202.5 402H-6.5C-6.5 402 -77 402 -77 335.5C-77 269 -6.5 269 -6.5 269H202.5Z"
+ stroke="#00AE99"
+ strokeWidth="3"
+ />
+ </g>
+ </Image>
+);
+
+const moveUp = keyframes`
+ 0% { transform: translate3d(0, 0, 0) }
+ 45% { transform: translate3d(0, 0, 0) }
+ 55% { transform: translate3d(0, -7%, 0) }
+ 85% { transform: translate3d(0, -7%, 0) }
+ 100% { transform: translate3d(0, 0, 0) }
+`;
+
+const moveLeft = keyframes`
+ 0% { transform: translate3d(0, 0, 0) }
+ 45% { transform: translate3d(0, 0, 0) }
+ 55% { transform: translate3d(-7%, 0, 0) }
+ 85% { transform: translate3d(-7%, 0, 0) }
+ 100% { transform: translate3d(0, 0, 0) }
+`;
+
+const moveDiag = keyframes`
+ 0% { transform: translate3d(0, 0, 0) }
+ 45% { transform: translate3d(0, 0, 0) }
+ 55% { transform: translate3d(5%, 5%, 0) }
+ 85% { transform: translate3d(5%, 5%, 0) }
+ 100% { transform: translate3d(0, 0, 0) }
+`;
+
+const moveRight = keyframes`
+ 0% { transform: translate3d(0, 0, 0) }
+ 45% { transform: translate3d(0, 0, 0) }
+ 55% { transform: translate3d(7%, 0, 0) }
+ 85% { transform: translate3d(7%, 0, 0) }
+ 100% { transform: translate3d(0, 0, 0) }
+`;
+
+const spin = keyframes`
+ 0% { transform: rotate(0deg) }
+ 65% { transform: rotate(0deg) }
+ 85% { transform: rotate(90deg) }
+ 100% { transform: rotate(90deg) }
+`;
+
+const moveIn = keyframes`
+ 0% { opacity: 0; transform: scale(1.7) rotate(-30deg) }
+ 100% { opacity: 1; transform: scale(1) rotate(0deg) }
+`;
+
+const Image = styled.svg`
+ opacity: 0;
+ transform: scale(1.5) rotate(-30deg);
+ animation: ${moveIn} 2s forwards;
+`;
+
+const TopCircle = styled.circle`
+ animation: ${moveUp} 4s -2.85s infinite;
+`;
+const LeftCircle = styled.circle`
+ animation: ${moveLeft} 4s -2.85s infinite;
+`;
+const Oblong = styled.path`
+ animation: ${moveLeft} 4s -2.85s infinite;
+`;
+const Square = styled.path`
+ animation: ${moveDiag} 4s -2.85s infinite;
+`;
+const Rectangle = styled.path`
+ animation: ${moveRight} 4s -2.85s infinite;
+`;
+
+const Logo = styled.path`
+ animation: ${spin} 4s -2.8s infinite;
+ transform-origin: 202px 202.7px;
+`;
diff --git a/packages/website/ts/components/heroImage.tsx b/packages/website/ts/components/heroImage.tsx
new file mode 100644
index 000000000..af7c055ac
--- /dev/null
+++ b/packages/website/ts/components/heroImage.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+interface Props {
+ image: React.ReactNode;
+}
+
+export const LandingAnimation = (props: Props) => <Wrap>{props.image}</Wrap>;
+
+const Wrap = styled.figure`
+ display: inline-block;
+
+ svg {
+ width: 100%;
+ height: auto;
+ }
+
+ @media (min-width: 768px) {
+ width: 100%;
+ max-width: 400px;
+ }
+
+ @media (max-width: 768px) {
+ width: 180px;
+ margin-bottom: 40px;
+ }
+`;
diff --git a/packages/website/ts/components/icon.tsx b/packages/website/ts/components/icon.tsx
new file mode 100644
index 000000000..fc9d488f9
--- /dev/null
+++ b/packages/website/ts/components/icon.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import Loadable from 'react-loadable';
+import styled from 'styled-components';
+
+import { Paragraph } from 'ts/@next/components/text';
+import { getCSSPadding, PaddingInterface } from 'ts/@next/constants/utilities';
+
+interface IconProps extends PaddingInterface {
+ name?: string;
+ component?: React.ReactNode;
+ size?: 'small' | 'medium' | 'large' | 'hero' | number;
+}
+
+export const Icon: React.FunctionComponent<IconProps> = (props: IconProps) => {
+ if (props.name && !props.component) {
+ const IconSVG = Loadable({
+ loader: async () => import(/* webpackChunkName: "icon" */ `ts/@next/icons/illustrations/${props.name}.svg`),
+ loading: () => <Paragraph>Loading</Paragraph>,
+ });
+
+ return (
+ <StyledIcon {...props}>
+ <IconSVG />
+ </StyledIcon>
+ );
+ }
+
+ if (props.component) {
+ return <StyledIcon {...props}>{props.component}</StyledIcon>;
+ }
+
+ return null;
+};
+
+export const InlineIconWrap =
+ styled.div <
+ PaddingInterface >
+ `
+ margin: ${props => getCSSPadding(props.margin)};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ > figure {
+ margin: 0 5px;
+ }
+`;
+
+const _getSize = (size: string | number = 'small'): string => {
+ if (typeof size === 'string') {
+ return `var(--${size}Icon)`;
+ }
+
+ return `${size}px`;
+};
+
+const StyledIcon =
+ styled.figure <
+ IconProps >
+ `
+ width: ${props => _getSize(props.size)};
+ height: ${props => _getSize(props.size)};
+ margin: ${props => getCSSPadding(props.margin)};
+ display: inline-block;
+ flex-shrink: 0;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+`;
diff --git a/packages/website/ts/components/image.tsx b/packages/website/ts/components/image.tsx
new file mode 100644
index 000000000..65b2a9705
--- /dev/null
+++ b/packages/website/ts/components/image.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+interface Props {
+ alt?: string;
+ src?: any;
+ srcset?: any;
+ isCentered?: boolean;
+}
+
+const ImageClass: React.FunctionComponent<Props> = (props: Props) => {
+ return <img {...props} />;
+};
+
+export const Image =
+ styled(ImageClass) <
+ Props >
+ `
+ margin: ${props => props.isCentered && `0 auto`};
+`;
diff --git a/packages/website/ts/components/layout.tsx b/packages/website/ts/components/layout.tsx
new file mode 100644
index 000000000..770ee159c
--- /dev/null
+++ b/packages/website/ts/components/layout.tsx
@@ -0,0 +1,177 @@
+import styled from 'styled-components';
+import { getCSSPadding, PADDING_SIZES, PaddingInterface } from 'ts/@next/constants/utilities';
+
+interface WrapWidths {
+ default: string;
+ full: string;
+ medium: string;
+ narrow: string;
+ [key: string]: string;
+}
+
+interface ColumnWidths {
+ [key: string]: string;
+}
+
+interface SectionProps {
+ isNoPadding?: boolean;
+ isPadLarge?: boolean;
+ isNoMargin?: boolean;
+ bgColor?: string;
+ isFullWidth?: boolean;
+ isRelative?: boolean;
+}
+
+interface WrapProps extends PaddingInterface {
+ width?: 'default' | 'full' | 'medium' | 'narrow';
+ bgColor?: string;
+ isWrapped?: boolean;
+ isCentered?: boolean;
+ isReversed?: boolean;
+}
+
+interface ColumnProps {
+ colWidth?: '1/4' | '1/3' | '1/2' | '2/3';
+ isNoPadding?: boolean;
+ isNoMargin?: boolean;
+ isPadLarge?: boolean;
+ isFlexGrow?: boolean;
+ isMobileCentered?: boolean;
+ bgColor?: string;
+}
+
+interface GetColWidthArgs {
+ span?: number;
+ columns: number;
+}
+
+export interface WrapStickyInterface {
+ offsetTop?: string;
+}
+
+const _getColumnWidth = (args: GetColWidthArgs): string => {
+ const { span = 1, columns } = args;
+ const percentWidth = span / columns * 100;
+ const gutterDiff = GUTTER * (columns - 1) / columns;
+ return `calc(${percentWidth}% - ${gutterDiff}px)`;
+};
+
+const GUTTER = 30 as number;
+const MAX_WIDTH = 1500;
+export const BREAKPOINTS = {
+ mobile: '768px',
+};
+const WRAPPER_WIDTHS: WrapWidths = {
+ default: `${MAX_WIDTH}px`, // tbd
+ full: '100%',
+ medium: '1136px',
+ narrow: '930px',
+};
+const COLUMN_WIDTHS: ColumnWidths = {
+ '1/4': _getColumnWidth({ columns: 4 }),
+ '1/3': _getColumnWidth({ columns: 3 }),
+ '1/2': _getColumnWidth({ columns: 2 }),
+ '2/3': _getColumnWidth({ span: 2, columns: 3 }),
+};
+
+export const Main = styled.main`
+ max-width: ${MAX_WIDTH}px;
+ margin: 0 auto;
+
+ @media (min-width: ${BREAKPOINTS.mobile}) {
+ width: calc(100% - 60px);
+ }
+`;
+
+// We can also turn Section into a stateless comp,
+// passing a asElement (same patter nas Heading) so we dont have to
+// make a const on every route to withComponent-size it.
+// just <Section asElement?="div/section/footer/header/whatever" /> ?
+export const Section =
+ styled.section <
+ SectionProps >
+ `
+ width: ${props => (props.isFullWidth ? `calc(100% + ${GUTTER * 2}px)` : '100%')};
+ padding: ${props => !props.isNoPadding && (props.isPadLarge ? `${PADDING_SIZES.large}` : PADDING_SIZES.default)};
+ background-color: ${props => props.bgColor};
+ position: ${props => props.isRelative && 'relative'};
+ overflow: ${props => props.isRelative && 'hidden'};
+ margin-bottom: ${props => !props.isNoMargin && `${GUTTER}px`};
+
+ @media (min-width: 1560px) {
+ width: ${props => props.isFullWidth && '100vw'};
+ margin-left: ${props => props.isFullWidth && `calc(750px - 50vw)`};
+ }
+
+ @media (max-width: ${BREAKPOINTS.mobile}) {
+ margin-bottom: ${props => !props.isNoMargin && `${GUTTER / 2}px`};
+ padding: ${props =>
+ props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default};
+ }
+`;
+
+const WrapBase =
+ styled.div <
+ WrapProps >
+ `
+ max-width: ${props => WRAPPER_WIDTHS[props.width || 'default']};
+ padding: ${props => props.padding && getCSSPadding(props.padding)};
+ background-color: ${props => props.bgColor};
+ margin: 0 auto;
+`;
+
+export const Wrap = styled(WrapBase)`
+ @media (min-width: ${BREAKPOINTS.mobile}) {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ flex-direction: ${props => props.isReversed && 'row-reverse'};
+ }
+`;
+
+export const WrapCentered = styled(WrapBase)`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+`;
+
+export const WrapSticky =
+ styled.div <
+ WrapStickyInterface >
+ `
+ position: sticky;
+ top: ${props => props.offsetTop || '60px'};
+`;
+
+export const WrapGrid = styled(WrapBase)`
+ display: flex;
+ flex-wrap: ${props => props.isWrapped && `wrap`};
+ justify-content: ${props => (props.isCentered ? `center` : 'space-between')};
+`;
+
+export const Column =
+ styled.div <
+ ColumnProps >
+ `
+ background-color: ${props => props.bgColor};
+ flex-grow: ${props => props.isFlexGrow && 1};
+
+ @media (min-width: ${BREAKPOINTS.mobile}) {
+ padding: ${props =>
+ !props.isNoPadding &&
+ (props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default)};
+ width: ${props => (props.colWidth ? COLUMN_WIDTHS[props.colWidth] : '100%')};
+ }
+
+ @media (max-width: ${BREAKPOINTS.mobile}) {
+ padding: ${props => !props.isNoPadding && (props.isPadLarge ? '40px 30px' : 0)};
+ margin-bottom: 20px;
+ text-align: ${props => props.isMobileCentered && 'center'};
+ }
+`;
+
+WrapGrid.defaultProps = {
+ isCentered: true,
+};
diff --git a/packages/website/ts/components/link.tsx b/packages/website/ts/components/link.tsx
new file mode 100644
index 000000000..080a0abcc
--- /dev/null
+++ b/packages/website/ts/components/link.tsx
@@ -0,0 +1,64 @@
+import { Link as SmartLink } from '@0x/react-shared';
+import * as React from 'react';
+import styled from 'styled-components';
+
+interface LinkInterface {
+ color?: string;
+ children?: React.ReactNode | string;
+ isNoArrow?: boolean;
+ hasIcon?: boolean | string;
+ isBlock?: boolean;
+ isCentered?: boolean;
+ href?: string;
+ theme?: {
+ textColor: string;
+ };
+ shouldOpenInNewTab?: boolean;
+ target?: string;
+}
+
+export const Link = (props: LinkInterface) => {
+ const { children, isNoArrow, href } = props;
+
+ return (
+ <StyledLink to={href} {...props}>
+ {children}
+ {!isNoArrow && (
+ <svg width="25" height="25" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path
+ d="M8.484 5.246l.023 1.411 8.147.053L4.817 18.547l.996.996L17.65 7.706l.052 8.146 1.411.024-.068-10.561-10.561-.069z"
+ fill="currentColor"
+ />
+ </svg>
+ )}
+ </StyledLink>
+ );
+};
+
+// Added this, & + & doesnt really work since we switch with element types...
+export const LinkWrap = styled.div`
+ a + a,
+ a + button,
+ button + a {
+ margin-left: 20px;
+ }
+`;
+
+const StyledLink =
+ styled(SmartLink) <
+ LinkInterface >
+ `
+ display: ${props => !props.isBlock && 'inline-flex'};
+ color: ${props => props.color || props.theme.linkColor};
+ text-align: center;
+ font-size: 18px;
+ text-decoration: none;
+ align-items: center;
+
+ @media (max-width: 768px) {
+ }
+
+ svg {
+ margin-left: 3px;
+ }
+`;
diff --git a/packages/website/ts/components/logo.tsx b/packages/website/ts/components/logo.tsx
new file mode 100644
index 000000000..227d48ee0
--- /dev/null
+++ b/packages/website/ts/components/logo.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { ThemeInterface } from 'ts/@next/components/siteWrap';
+import LogoIcon from 'ts/@next/icons/logo-with-type.svg';
+
+interface LogoInterface {
+ theme?: ThemeInterface;
+}
+
+// Note let's refactor this
+// is it absolutely necessary to have a stateless component
+// to pass props down into the styled icon?
+const StyledLogo = styled.div`
+ text-align: left;
+ position: relative;
+ z-index: 25;
+
+ @media (max-width: 800px) {
+ svg {
+ width: 60px;
+ }
+ }
+`;
+
+const Icon =
+ styled(LogoIcon) <
+ LogoInterface >
+ `
+ flex-shrink: 0;
+
+ path {
+ fill: ${props => props.theme.textColor};
+ }
+`;
+
+export const Logo: React.StatelessComponent<LogoInterface> = (props: LogoInterface) => (
+ <StyledLogo>
+ <Icon {...props} />
+ </StyledLogo>
+);
diff --git a/packages/website/ts/components/mobileNav.tsx b/packages/website/ts/components/mobileNav.tsx
new file mode 100644
index 000000000..13cf46fca
--- /dev/null
+++ b/packages/website/ts/components/mobileNav.tsx
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import MediaQuery from 'react-responsive';
+import styled from 'styled-components';
+
+import { Link } from 'react-router-dom';
+
+import { WrapGrid, WrapProps } from 'ts/@next/components/newLayout';
+import { WebsitePaths } from 'ts/types';
+
+interface Props {
+ isToggled: boolean;
+ toggleMobileNav: () => void;
+}
+
+export class MobileNav extends React.PureComponent<Props> {
+ public render(): React.ReactNode {
+ const { isToggled, toggleMobileNav } = this.props;
+
+ return (
+ <MediaQuery maxWidth={800}>
+ <Wrap isToggled={isToggled}>
+ <Section>
+ <h4>Products</h4>
+
+ <ul>
+ <li>
+ <Link to={WebsitePaths.Instant}>0x Instant</Link>
+ </li>
+ <li>
+ <Link to={WebsitePaths.LaunchKit}>0x Launch Kit</Link>
+ </li>
+ </ul>
+ </Section>
+
+ <Section isDark={true}>
+ <Grid as="ul" isFullWidth={true} isWrapped={true}>
+ <li>
+ <Link to={WebsitePaths.Why}>Why 0x</Link>
+ </li>
+ <li>
+ <Link to={WebsitePaths.AboutMission}>About</Link>
+ </li>
+ <li>
+ <a href="https://blog.0xproject.com/latest" target="_blank">
+ Blog
+ </a>
+ </li>
+ </Grid>
+ </Section>
+
+ {isToggled && <Overlay onClick={toggleMobileNav} />}
+ </Wrap>
+ </MediaQuery>
+ );
+ }
+}
+
+const Wrap =
+ styled.nav <
+ { isToggled: boolean } >
+ `
+ width: 100%;
+ height: 357px;
+ background-color: ${props => props.theme.mobileNavBgUpper};
+ color: ${props => props.theme.mobileNavColor};
+ transition: ${props => (props.isToggled ? 'visibility 0s, transform 0.5s' : 'visibility 0s 0.5s, transform 0.5s')};
+ transform: translate3d(0, ${props => (props.isToggled ? 0 : '-100%')}, 0);
+ visibility: ${props => !props.isToggled && 'hidden'};
+ position: fixed;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ z-index: 20;
+ top: 0;
+ left: 0;
+ font-size: 20px;
+
+ a {
+ padding: 15px 0;
+ display: block;
+ color: inherit;
+ }
+
+ h4 {
+ font-size: 14px;
+ opacity: 0.5;
+ }
+`;
+
+const Overlay = styled.div`
+ position: absolute;
+ width: 100vw;
+ height: 100vh;
+ top: 100%;
+ background: transparent;
+ cursor: pointer;
+`;
+
+interface SectionProps {
+ isDark?: boolean;
+}
+const Section =
+ styled.div <
+ SectionProps >
+ `
+ width: 100%;
+ padding: 15px 30px;
+ background-color: ${props => (props.isDark ? props.theme.mobileNavBgLower : 'transparent')};
+`;
+
+const Grid =
+ styled(WrapGrid) <
+ WrapProps >
+ `
+ justify-content: flex-start;
+
+ li {
+ width: 50%;
+ flex-shrink: 0;
+ }
+`;
diff --git a/packages/website/ts/components/modals/input.tsx b/packages/website/ts/components/modals/input.tsx
new file mode 100644
index 000000000..8cfcc9763
--- /dev/null
+++ b/packages/website/ts/components/modals/input.tsx
@@ -0,0 +1,95 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+export enum InputWidth {
+ Half,
+ Full,
+}
+
+interface InputProps {
+ name: string;
+ width?: InputWidth;
+ label: string;
+ type?: string;
+ errors?: ErrorProps;
+ isErrors?: boolean;
+ required?: boolean;
+}
+
+interface ErrorProps {
+ [key: string]: string;
+}
+
+export const Input = React.forwardRef((props: InputProps, ref?: React.Ref<HTMLInputElement>) => {
+ const { name, label, type, errors } = props;
+ const id = `input-${name}`;
+ const componentType = type === 'textarea' ? 'textarea' : 'input';
+ const isErrors = errors.hasOwnProperty(name) && errors[name] !== null;
+ const errorMessage = isErrors ? errors[name] : null;
+
+ return (
+ <InputWrapper {...props}>
+ <Label htmlFor={id}>{label}</Label>
+ <StyledInput as={componentType} ref={ref} id={id} isErrors={isErrors} {...props} />
+ {isErrors && <Error>{errorMessage}</Error>}
+ </InputWrapper>
+ );
+});
+
+Input.defaultProps = {
+ width: InputWidth.Full,
+ errors: {},
+};
+
+const StyledInput = styled.input`
+ appearance: none;
+ background-color: #fff;
+ border: 1px solid #d5d5d5;
+ color: #000;
+ font-size: 1.294117647rem;
+ padding: 16px 15px 14px;
+ outline: none;
+ width: 100%;
+ min-height: ${props => props.type === 'textarea' && `120px`};
+
+ background-color: ${(props: InputProps) => props.isErrors && `#FDEDED`};
+ border-color: ${(props: InputProps) => props.isErrors && `#FD0000`};
+
+ &::placeholder {
+ color: #c3c3c3;
+ }
+`;
+
+const InputWrapper =
+ styled.div <
+ InputProps >
+ `
+ position: relative;
+ flex-grow: ${props => props.width === InputWidth.Full && 1};
+ width: ${props => props.width === InputWidth.Half && `calc(50% - 15px)`};
+
+ @media (max-width: 768px) {
+ width: 100%;
+ margin-bottom: 30px;
+ }
+`;
+
+const Label = styled.label`
+ color: #000;
+ font-size: 1.111111111rem;
+ line-height: 1.4em;
+ margin-bottom: 10px;
+ display: inline-block;
+`;
+
+const Error = styled.span`
+ color: #fd0000;
+ font-size: 0.833333333rem;
+ line-height: 1em;
+ display: inline-block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ transform: translateY(24px);
+`;
diff --git a/packages/website/ts/components/modals/modal_contact.tsx b/packages/website/ts/components/modals/modal_contact.tsx
new file mode 100644
index 000000000..b97baf5e7
--- /dev/null
+++ b/packages/website/ts/components/modals/modal_contact.tsx
@@ -0,0 +1,278 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { colors } from 'ts/style/colors';
+
+import { DialogContent, DialogOverlay } from '@reach/dialog';
+import '@reach/dialog/styles.css';
+
+import { Button } from 'ts/@next/components/button';
+import { Icon } from 'ts/@next/components/icon';
+import { Input, InputWidth } from 'ts/@next/components/modals/input';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+import { GlobalStyle } from 'ts/@next/constants/globalStyle';
+
+interface Props {
+ theme?: GlobalStyle;
+ isOpen?: boolean;
+ onDismiss?: () => void;
+}
+
+interface FormProps {
+ isSuccessful?: boolean;
+ isSubmitting?: boolean;
+}
+
+interface ErrorResponseProps {
+ param: string;
+ location: string;
+ msg: string;
+}
+
+interface ErrorResponse {
+ errors: ErrorResponseProps[];
+}
+
+interface ErrorProps {
+ [key: string]: string;
+}
+
+export class ModalContact extends React.Component<Props> {
+ public state = {
+ isSubmitting: false,
+ isSuccessful: false,
+ errors: {},
+ };
+ public nameRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public emailRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public companyProjectRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public linkRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public commentsRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public constructor(props: Props) {
+ super(props);
+ }
+ public render(): React.ReactNode {
+ const { isOpen, onDismiss } = this.props;
+ const { isSuccessful, errors } = this.state;
+
+ return (
+ <>
+ <DialogOverlay
+ style={{ background: 'rgba(0, 0, 0, 0.75)', zIndex: 30 }}
+ isOpen={isOpen}
+ onDismiss={onDismiss}
+ >
+ <StyledDialogContent>
+ <Form onSubmit={this._onSubmitAsync.bind(this)} isSuccessful={isSuccessful}>
+ <Heading color={colors.textDarkPrimary} size={34} asElement="h2">
+ Contact the 0x Core Team
+ </Heading>
+ <Paragraph isMuted={true} color={colors.textDarkPrimary}>
+ If you're considering building on 0x, we're happy to answer your questions. Fill out the
+ form so we can connect you with the right person to help you get started.
+ </Paragraph>
+ <InputRow>
+ <Input
+ name="name"
+ label="Your name"
+ type="text"
+ width={InputWidth.Half}
+ ref={this.nameRef}
+ required={true}
+ errors={errors}
+ />
+ <Input
+ name="email"
+ label="Your email"
+ type="email"
+ ref={this.emailRef}
+ required={true}
+ errors={errors}
+ width={InputWidth.Half}
+ />
+ </InputRow>
+ <InputRow>
+ <Input
+ name="companyOrProject"
+ label="Name of your project / company"
+ type="text"
+ ref={this.companyProjectRef}
+ required={true}
+ errors={errors}
+ />
+ </InputRow>
+ <InputRow>
+ <Input
+ name="link"
+ label="Do you have any documentation or a website?"
+ type="text"
+ ref={this.linkRef}
+ errors={errors}
+ />
+ </InputRow>
+ <InputRow>
+ <Input
+ name="comments"
+ label="Anything else?"
+ type="textarea"
+ ref={this.commentsRef}
+ errors={errors}
+ />
+ </InputRow>
+ <ButtonRow>
+ <Button
+ color="#5C5C5C"
+ isNoBorder={true}
+ isTransparent={true}
+ type="button"
+ onClick={this.props.onDismiss}
+ >
+ Back
+ </Button>
+ <Button>Submit</Button>
+ </ButtonRow>
+ </Form>
+ <Confirmation isSuccessful={isSuccessful}>
+ <Icon name="rocketship" size="large" margin={[0, 0, 'default', 0]} />
+ <Heading color={colors.textDarkPrimary} size={34} asElement="h2">
+ Thanks for contacting us.
+ </Heading>
+ <Paragraph isMuted={true} color={colors.textDarkPrimary}>
+ We'll get back to you soon. If you need quick support in the meantime, reach out to the
+ 0x team on Discord.
+ </Paragraph>
+ <Button onClick={this.props.onDismiss}>Done</Button>
+ </Confirmation>
+ </StyledDialogContent>
+ </DialogOverlay>
+ </>
+ );
+ }
+ private async _onSubmitAsync(e: Event): Promise<void> {
+ e.preventDefault();
+
+ const name = this.nameRef.current.value;
+ const email = this.emailRef.current.value;
+ const projectOrCompany = this.companyProjectRef.current.value;
+ const link = this.linkRef.current.value;
+ const comments = this.commentsRef.current.value;
+
+ this.setState({ ...this.state, errors: [], isSubmitting: true });
+
+ try {
+ // Disabling no-unbound method b/c no reason for _.isEmpty to be bound
+ // tslint:disable:no-unbound-method
+ const response = await fetch('https://website-api.0xproject.com/leads', {
+ method: 'post',
+ mode: 'cors',
+ credentials: 'same-origin',
+ headers: {
+ 'content-type': 'application/json; charset=utf-8',
+ },
+ body: JSON.stringify(_.omitBy({ name, email, projectOrCompany, link, comments }, _.isEmpty)),
+ });
+
+ if (!response.ok) {
+ const errorResponse: ErrorResponse = await response.json();
+ const errors = this._parseErrors(errorResponse.errors);
+ this.setState({ ...this.state, isSubmitting: false, errors });
+
+ throw new Error('Request failed');
+ }
+
+ this.setState({ ...this.state, isSuccessful: true });
+ } catch (e) {
+ // Empty block
+ }
+ }
+ private _parseErrors(errors: ErrorResponseProps[]): ErrorProps {
+ const initialValue: {} = {};
+ return _.reduce(
+ errors,
+ (hash: ErrorProps, error: ErrorResponseProps) => {
+ const { param, msg } = error;
+ const key = param;
+ hash[key] = msg;
+
+ return hash;
+ },
+ initialValue,
+ );
+ }
+}
+// Handle errors: {"errors":[{"location":"body","param":"name","msg":"Invalid value"},{"location":"body","param":"email","msg":"Invalid value"}]}
+
+const InputRow = styled.div`
+ width: 100%;
+ flex: 0 0 auto;
+
+ @media (min-width: 768px) {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 30px;
+ }
+`;
+
+const ButtonRow = styled(InputRow)`
+ @media (max-width: 768px) {
+ display: flex;
+ flex-direction: column;
+
+ button:nth-child(1) {
+ order: 2;
+ }
+
+ button:nth-child(2) {
+ order: 1;
+ margin-bottom: 10px;
+ }
+ }
+`;
+
+const StyledDialogContent = styled(DialogContent)`
+ position: relative;
+ max-width: 800px;
+ background-color: #f6f6f6 !important;
+ padding: 60px 60px !important;
+
+ @media (max-width: 768px) {
+ width: calc(100vw - 40px) !important;
+ margin: 40px auto !important;
+ padding: 30px 30px !important;
+ }
+`;
+
+const Form =
+ styled.form <
+ FormProps >
+ `
+ position: relative;
+ transition: opacity 0.30s ease-in-out, visibility 0.30s ease-in-out;
+
+ opacity: ${props => props.isSuccessful && `0`};
+ visibility: ${props => props.isSuccessful && `hidden`};
+`;
+
+const Confirmation =
+ styled.div <
+ FormProps >
+ `
+ position: absolute;
+ top: 50%;
+ text-align: center;
+ width: 100%;
+ left: 0;
+ transition: opacity 0.30s ease-in-out, visibility 0.30s ease-in-out;
+ transition-delay: 0.40s;
+ padding: 60px 60px;
+ transform: translateY(-50%);
+ opacity: ${props => (props.isSuccessful ? `1` : `0`)};
+ visibility: ${props => (props.isSuccessful ? 'visible' : `hidden`)};
+
+ p {
+ max-width: 492px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+`;
diff --git a/packages/website/ts/components/newLayout.tsx b/packages/website/ts/components/newLayout.tsx
new file mode 100644
index 000000000..28e7653c5
--- /dev/null
+++ b/packages/website/ts/components/newLayout.tsx
@@ -0,0 +1,152 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+export interface WrapProps {
+ bgColor?: string;
+ id?: string;
+ offsetTop?: string;
+ maxWidth?: string;
+ wrapWidth?: string;
+ isFullWidth?: boolean;
+ isTextCentered?: boolean;
+ isCentered?: boolean;
+ isWrapped?: boolean;
+}
+
+export interface WrapGridProps {
+ isWrapped?: boolean;
+ isCentered?: boolean;
+}
+
+export interface WrapStickyProps {
+ offsetTop?: string;
+}
+
+export interface SectionProps extends WrapProps {
+ isPadded?: boolean;
+ isFullWidth?: boolean;
+ isFlex?: boolean;
+ padding?: string;
+ paddingMobile?: string;
+ flexBreakpoint?: string;
+ maxWidth?: string;
+ bgColor?: 'dark' | 'light' | string;
+ children: any;
+}
+
+export interface FlexProps {
+ padding?: string;
+ isFlex?: boolean;
+ flexBreakpoint?: string;
+}
+
+export interface ColumnProps {
+ padding?: string;
+ width?: string;
+ maxWidth?: string;
+}
+
+export const Section: React.FunctionComponent<SectionProps> = (props: SectionProps) => {
+ return (
+ <SectionBase {...props}>
+ <Wrap {...props}>{props.children}</Wrap>
+ </SectionBase>
+ );
+};
+
+export const Column =
+ styled.div <
+ ColumnProps >
+ `
+ width: ${props => props.width};
+ max-width: ${props => props.maxWidth};
+ padding: ${props => props.padding};
+
+ @media (max-width: 768px) {
+ width: 100%;
+
+ & + & {
+ margin-top: 60px;
+ }
+ }
+`;
+
+export const FlexWrap =
+ styled.div <
+ FlexProps >
+ `
+ max-width: 1500px;
+ margin: 0 auto;
+ padding: ${props => props.padding};
+
+ @media (min-width: ${props => props.flexBreakpoint || '768px'}) {
+ display: ${props => props.isFlex && 'flex'};
+ justify-content: ${props => props.isFlex && 'space-between'};
+ }
+`;
+
+export const WrapSticky =
+ styled.div <
+ WrapProps >
+ `
+ position: sticky;
+ top: ${props => props.offsetTop || '60px'};
+`;
+
+const SectionBase =
+ styled.section <
+ SectionProps >
+ `
+ width: ${props => !props.isFullWidth && 'calc(100% - 60px)'};
+ max-width: 1500px;
+ margin: 0 auto;
+ padding: ${props => props.isPadded && '120px 0'};
+ background-color: ${props => props.theme[`${props.bgColor}BgColor`] || props.bgColor};
+ position: relative;
+ overflow: ${props => !props.isFullWidth && 'hidden'};
+
+ @media (max-width: 768px) {
+ padding: ${props => props.isPadded && (props.paddingMobile || '40px 0')};
+ }
+`;
+
+const Wrap =
+ styled(FlexWrap) <
+ WrapProps >
+ `
+ width: ${props => props.wrapWidth || 'calc(100% - 60px)'};
+ width: ${props => props.bgColor && 'calc(100% - 60px)'};
+ max-width: ${props => !props.isFullWidth && (props.maxWidth || '895px')};
+ text-align: ${props => props.isTextCentered && 'center'};
+ margin: 0 auto;
+`;
+
+export const WrapGrid =
+ styled(Wrap) <
+ WrapProps >
+ `
+ display: flex;
+ flex-wrap: ${props => props.isWrapped && `wrap`};
+ justify-content: ${props => (props.isCentered ? `center` : 'space-between')};
+
+ @media (max-width: 768px) {
+ width: 100%;
+ }
+`;
+
+Section.defaultProps = {
+ isPadded: true,
+};
+
+FlexWrap.defaultProps = {
+ isFlex: true,
+};
+
+WrapGrid.defaultProps = {
+ isCentered: true,
+ isFullWidth: true,
+};
+
+Wrap.defaultProps = {
+ isFlex: false,
+};
diff --git a/packages/website/ts/components/newsletter_form.tsx b/packages/website/ts/components/newsletter_form.tsx
new file mode 100644
index 000000000..8572ccc5f
--- /dev/null
+++ b/packages/website/ts/components/newsletter_form.tsx
@@ -0,0 +1,202 @@
+import * as React from 'react';
+import styled, { withTheme } from 'styled-components';
+
+import { ThemeValuesInterface } from 'ts/@next/components/siteWrap';
+import { colors } from 'ts/style/colors';
+import { errorReporter } from 'ts/utils/error_reporter';
+
+interface FormProps {
+ theme: ThemeValuesInterface;
+}
+
+interface InputProps {
+ isSubmitted: boolean;
+ name: string;
+ type: string;
+ label: string;
+ textColor: string;
+ required?: boolean;
+}
+
+interface ArrowProps {
+ isSubmitted: boolean;
+}
+
+const Input = React.forwardRef((props: InputProps, ref: React.Ref<HTMLInputElement>) => {
+ const { name, label, type } = props;
+ const id = `input-${name}`;
+
+ return (
+ <InnerInputWrapper {...props}>
+ <label className="visuallyHidden" htmlFor={id}>
+ {label}
+ </label>
+ <StyledInput ref={ref} id={id} placeholder={label} type={type || 'text'} {...props} />
+ </InnerInputWrapper>
+ );
+});
+
+class Form extends React.Component<FormProps> {
+ public emailInput = React.createRef<HTMLInputElement>();
+ public state = {
+ isSubmitted: false,
+ };
+ public render(): React.ReactNode {
+ const { isSubmitted } = this.state;
+ const { theme } = this.props;
+
+ return (
+ <StyledForm onSubmit={this._onSubmitAsync.bind(this)}>
+ <InputWrapper>
+ <Input
+ isSubmitted={isSubmitted}
+ name="email"
+ type="email"
+ label="Email Address"
+ ref={this.emailInput}
+ required={true}
+ textColor={theme.textColor}
+ />
+
+ <SubmitButton>
+ <Arrow
+ isSubmitted={isSubmitted}
+ width="22"
+ height="17"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M13.066 0l-1.068 1.147 6.232 6.557H0v1.592h18.23l-6.232 6.557L13.066 17l8.08-8.5-8.08-8.5z"
+ fill="#CBCBCB"
+ />
+ </Arrow>
+ </SubmitButton>
+ <SuccessText isSubmitted={isSubmitted}>🎉 Thank you for signing up!</SuccessText>
+ </InputWrapper>
+ <Text>Subscribe to our newsletter for updates in the 0x ecosystem</Text>
+ </StyledForm>
+ );
+ }
+
+ private async _onSubmitAsync(e: React.FormEvent<HTMLFormElement>): Promise<void> {
+ e.preventDefault();
+
+ const email = this.emailInput.current.value;
+ const referrer = 'https://0x.org/';
+
+ this.setState({ isSubmitted: true });
+
+ if (email === 'triggererror@0xproject.org') {
+ throw new Error('Manually triggered error');
+ }
+
+ try {
+ await fetch('https://website-api.0x.org/newsletter_subscriber/substack', {
+ method: 'post',
+ mode: 'cors',
+ headers: {
+ 'content-type': 'application/json; charset=utf-8',
+ },
+ body: JSON.stringify({ email, referrer }),
+ });
+ } catch (e) {
+ errorReporter.report(e);
+ }
+ }
+}
+
+export const NewsletterForm = withTheme(Form);
+
+const StyledForm = styled.form`
+ appearance: none;
+ border: 0;
+ color: ${colors.white};
+ padding: 13px 0 14px;
+ margin-top: 27px;
+`;
+
+const StyledInput =
+ styled.input <
+ InputProps >
+ `
+ appearance: none;
+ background-color: transparent;
+ border: 0;
+ border-bottom: 1px solid #393939;
+ color: ${props => props.textColor || '#fff'};
+ font-size: 1.294117647rem;
+ padding: 15px 0;
+ outline: none;
+ width: 100%;
+
+ &::placeholder {
+ color: #B1B1B1; // #9D9D9D on light theme
+ }
+`;
+
+const InputWrapper = styled.div`
+ position: relative;
+`;
+
+const InnerInputWrapper =
+ styled.div <
+ ArrowProps >
+ `
+ opacity: ${props => props.isSubmitted && 0};
+ visibility: ${props => props.isSubmitted && 'hidden'};
+ transition: opacity 0.25s ease-in-out, visibility 0.25s ease-in-out;
+ transition-delay: 0.30s;
+`;
+
+const SubmitButton = styled.button`
+ width: 44px;
+ height: 44px;
+ background-color: transparent;
+ border: 0;
+ position: absolute;
+ right: 0;
+ top: calc(50% - 22px);
+ overflow: hidden;
+ outline: 0;
+
+ &:focus-within {
+ //background-color: #eee;
+ }
+`;
+
+const Text = styled.p`
+ color: #656565;
+ font-size: 0.833333333rem;
+ font-weight: 300;
+ line-height: 1.2em;
+ margin-top: 15px;
+`;
+
+const SuccessText =
+ styled.p <
+ ArrowProps >
+ `
+ color: #B1B1B1;
+ font-size: 1rem;
+ font-weight: 300;
+ line-height: 1.2em;
+ padding-top: 25px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ text-align: left;
+ right: 50px;
+ opacity: ${props => (props.isSubmitted ? 1 : 0)};
+ visibility: ${props => (props.isSubmitted ? 'visible' : 'hidden')};
+ transition: opacity 0.25s ease-in-out, visibility 0.25s ease-in-out;
+ transition-delay: 0.55s;
+`;
+
+const Arrow =
+ styled.svg <
+ ArrowProps >
+ `
+ transform: ${props => props.isSubmitted && `translateX(44px)`};
+ transition: transform 0.25s ease-in-out;
+`;
diff --git a/packages/website/ts/components/sections/landing/about.tsx b/packages/website/ts/components/sections/landing/about.tsx
new file mode 100644
index 000000000..7b51b0d13
--- /dev/null
+++ b/packages/website/ts/components/sections/landing/about.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { Button } from 'ts/@next/components/button';
+import { Icon, InlineIconWrap } from 'ts/@next/components/icon';
+import { Column, FlexWrap, Section } from 'ts/@next/components/newLayout';
+import { Paragraph } from 'ts/@next/components/text';
+import { WebsitePaths } from 'ts/types';
+
+interface FigureProps {
+ value: string;
+ description: string;
+}
+
+export const SectionLandingAbout = () => (
+ <Section bgColor="dark" isTextCentered={true}>
+ <InlineIconWrap>
+ <Icon name="descriptionCoin" size="small" />
+ <Icon name="descriptionCopy" size="small" />
+ <Icon name="descriptionFlask" size="small" />
+ <Icon name="descriptionBolt" size="small" />
+ </InlineIconWrap>
+
+ <Paragraph size="large" isCentered={true} isMuted={1} padding={['large', 0, 'default', 0]}>
+ Anyone in the world can use 0x to service a wide variety of markets ranging from gaming items to financial
+ instruments to assets that could have never existed before.
+ </Paragraph>
+
+ <DeveloperLink href={`${WebsitePaths.Why}#cases`} isWithArrow={true} isAccentColor={true}>
+ Discover how developers use 0x
+ </DeveloperLink>
+
+ <hr
+ style={{
+ width: '100%',
+ maxWidth: '340px',
+ borderColor: '#3C4746',
+ margin: '60px auto',
+ }}
+ />
+
+ <FlexWrap as="dl">
+ <Figure value="353K" description="Total Transactions" />
+
+ <Figure value="$447M" description="Total Volume" />
+
+ <Figure value="30+" description="Total Projects" />
+ </FlexWrap>
+ </Section>
+);
+
+const Figure = (props: FigureProps) => (
+ <Column padding="0 30px">
+ <FigureValue>{props.value}</FigureValue>
+ <FigureDescription>{props.description}</FigureDescription>
+ </Column>
+);
+
+const DeveloperLink = styled(Button)`
+ @media (max-width: 500px) {
+ && {
+ white-space: pre-wrap;
+ line-height: 1.3;
+ }
+ }
+`;
+
+const FigureValue = styled.dt`
+ font-size: 50px;
+ font-weight: 300;
+ margin-bottom: 15px;
+
+ @media (max-width: 768px) {
+ font-size: 40px;
+ }
+`;
+
+const FigureDescription = styled.dd`
+ font-size: 18px;
+ color: #999999;
+`;
diff --git a/packages/website/ts/components/sections/landing/clients.tsx b/packages/website/ts/components/sections/landing/clients.tsx
new file mode 100644
index 000000000..4170fde46
--- /dev/null
+++ b/packages/website/ts/components/sections/landing/clients.tsx
@@ -0,0 +1,113 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import styled from 'styled-components';
+import { Heading } from 'ts/@next/components/text';
+
+import { Section, WrapGrid } from 'ts/@next/components/newLayout';
+
+interface ProjectLogo {
+ name: string;
+ imageUrl?: string;
+ persistOnMobile?: boolean;
+}
+
+interface StyledProjectInterface {
+ isOnMobile?: boolean;
+}
+
+const projects: ProjectLogo[] = [
+ {
+ name: 'Radar Relay',
+ imageUrl: 'images/@next/clients/radar-relay.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'Paradex',
+ imageUrl: 'images/@next/clients/paradex.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'Star Bit Ex',
+ imageUrl: 'images/@next/clients/starbitex.svg',
+ },
+ {
+ name: 'LedgerDex',
+ imageUrl: 'images/@next/clients/ledgerdex.svg',
+ },
+ {
+ name: 'OpenRelay',
+ imageUrl: 'images/@next/clients/openrelay.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'Bamboo Relay',
+ imageUrl: 'images/@next/clients/bamboo.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'dEX',
+ imageUrl: 'images/@next/clients/ercdex.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'emoon',
+ imageUrl: 'images/@next/clients/emoon.svg',
+ persistOnMobile: true,
+ },
+ {
+ name: 'Gods Unchained',
+ imageUrl: 'images/@next/clients/godsUnchained.svg',
+ },
+ {
+ name: 'Instex',
+ imageUrl: 'images/@next/clients/instex.svg',
+ },
+ {
+ name: 'Lake Trade',
+ imageUrl: 'images/@next/clients/laketrade.svg',
+ },
+ {
+ name: 'Veil',
+ imageUrl: 'images/@next/clients/veil.svg',
+ },
+];
+
+export const SectionLandingClients = () => (
+ <Section isTextCentered={true}>
+ <Heading size="small">Join the growing number of projects developing on 0x</Heading>
+
+ <WrapGrid isWrapped={true}>
+ {_.map(projects, (item: ProjectLogo, index) => (
+ <StyledProject key={`client-${index}`} isOnMobile={item.persistOnMobile}>
+ <img src={item.imageUrl} alt={item.name} />
+ </StyledProject>
+ ))}
+ </WrapGrid>
+ </Section>
+);
+
+const StyledProject =
+ styled.div <
+ StyledProjectInterface >
+ `
+ flex-shrink: 0;
+
+ img {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+ }
+
+ @media (min-width: 768px) {
+ width: auto;
+ height: 50px;
+ margin: 30px;
+ }
+
+ @media (max-width: 768px) {
+ width: auto;
+ height: 42px;
+ margin: 15px;
+ display: ${props => !props.isOnMobile && 'none'};
+ }
+`;
diff --git a/packages/website/ts/components/sections/landing/cta.tsx b/packages/website/ts/components/sections/landing/cta.tsx
new file mode 100644
index 000000000..ec7f5d961
--- /dev/null
+++ b/packages/website/ts/components/sections/landing/cta.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react';
+
+import { BlockIconLink } from 'ts/@next/components/blockIconLink';
+import { Section } from 'ts/@next/components/newLayout';
+
+import { AnimatedChatIcon } from 'ts/@next/components/animatedChatIcon';
+import { AnimatedCompassIcon } from 'ts/@next/components/animatedCompassIcon';
+import { WebsitePaths } from 'ts/types';
+
+interface Props {
+ onContactClick?: () => void;
+}
+
+export const SectionLandingCta = (props: Props) => (
+ <Section isPadded={false} isFlex={true} maxWidth="auto" wrapWidth="100%" flexBreakpoint="900px">
+ <BlockIconLink
+ iconComponent={<AnimatedCompassIcon />}
+ title="Ready to build on 0x?"
+ linkLabel="Get Started"
+ linkUrl={WebsitePaths.Docs}
+ />
+ <BlockIconLink
+ iconComponent={<AnimatedChatIcon />}
+ title="Want help from the 0x team?"
+ linkLabel="Get in Touch"
+ linkAction={props.onContactClick}
+ />
+ </Section>
+);
diff --git a/packages/website/ts/components/sections/landing/hero.tsx b/packages/website/ts/components/sections/landing/hero.tsx
new file mode 100644
index 000000000..cf67ad66d
--- /dev/null
+++ b/packages/website/ts/components/sections/landing/hero.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+
+import { Button } from 'ts/@next/components/button';
+import { Hero } from 'ts/@next/components/hero';
+import { LandingAnimation } from 'ts/@next/components/heroImage';
+
+import { HeroAnimation } from 'ts/@next/components/heroAnimation';
+import { WebsitePaths } from 'ts/types';
+
+export const SectionLandingHero = () => (
+ <Hero
+ title="Powering Decentralized Exchange"
+ isLargeTitle={true}
+ isFullWidth={true}
+ description="0x is an open protocol that enables the peer-to-peer exchange of assets on the Ethereum blockchain."
+ figure={<LandingAnimation image={<HeroAnimation />} />}
+ actions={<HeroActions />}
+ />
+);
+
+const HeroActions = () => (
+ <>
+ <Button href="https://0x.org/docs" isInline={true}>
+ Get Started
+ </Button>
+
+ <Button to={WebsitePaths.Why} isTransparent={true} isInline={true}>
+ Learn More
+ </Button>
+ </>
+);
diff --git a/packages/website/ts/components/separator.tsx b/packages/website/ts/components/separator.tsx
new file mode 100644
index 000000000..0b8b8d766
--- /dev/null
+++ b/packages/website/ts/components/separator.tsx
@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+export const Separator = styled.hr`
+ background: #eaeaea;
+ height: 1px;
+ border: 0;
+`;
diff --git a/packages/website/ts/components/siteWrap.tsx b/packages/website/ts/components/siteWrap.tsx
new file mode 100644
index 000000000..75cb9a268
--- /dev/null
+++ b/packages/website/ts/components/siteWrap.tsx
@@ -0,0 +1,149 @@
+import * as React from 'react';
+import styled, { ThemeProvider } from 'styled-components';
+
+import { colors } from 'ts/style/colors';
+
+import { Footer } from 'ts/@next/components/footer';
+import { Header } from 'ts/@next/components/header';
+import { GlobalStyles } from 'ts/@next/constants/globalStyle';
+
+interface Props {
+ theme?: 'dark' | 'light' | 'gray';
+ children: any;
+}
+
+interface State {
+ isMobileNavOpen: boolean;
+}
+
+interface MainProps {
+ isNavToggled: boolean;
+}
+
+export interface ThemeValuesInterface {
+ bgColor: string;
+ darkBgColor?: string;
+ lightBgColor: string;
+ introTextColor: string;
+ textColor: string;
+ paragraphColor: string;
+ linkColor: string;
+ mobileNavBgUpper: string;
+ mobileNavBgLower: string;
+ mobileNavColor: string;
+ dropdownBg: string;
+ dropdownButtonBg: string;
+ dropdownBorderColor?: string;
+ dropdownColor: string;
+ headerButtonBg: string;
+ footerBg: string;
+ footerColor: string;
+}
+
+export interface ThemeInterface {
+ [key: string]: ThemeValuesInterface;
+}
+
+const GLOBAL_THEMES: ThemeInterface = {
+ dark: {
+ bgColor: '#000000',
+ darkBgColor: '#111A19',
+ lightBgColor: '#003831',
+ introTextColor: 'rgba(255, 255, 255, 0.75)',
+ textColor: '#FFFFFF',
+ paragraphColor: '#FFFFFF',
+ linkColor: colors.brandLight,
+ mobileNavBgUpper: '#003831',
+ mobileNavBgLower: '#022924',
+ mobileNavColor: '#FFFFFF',
+ dropdownBg: '#111A19',
+ dropdownButtonBg: '#003831',
+ dropdownColor: '#FFFFFF',
+ headerButtonBg: '#00AE99',
+ footerBg: '#181818',
+ footerColor: '#FFFFFF',
+ },
+ light: {
+ bgColor: '#FFFFFF',
+ lightBgColor: '#F3F6F4',
+ darkBgColor: '#003831',
+ introTextColor: 'rgba(92, 92, 92, 0.87)',
+ textColor: '#000000',
+ paragraphColor: '#474747',
+ linkColor: colors.brandDark,
+ mobileNavBgUpper: '#FFFFFF',
+ mobileNavBgLower: '#F3F6F4',
+ mobileNavColor: '#000000',
+ dropdownBg: '#FBFBFB',
+ dropdownButtonBg: '#F3F6F4',
+ dropdownColor: '#003831',
+ dropdownBorderColor: '#E4E4E4',
+ headerButtonBg: '#003831',
+ footerBg: '#F2F2F2',
+ footerColor: '#000000',
+ },
+ gray: {
+ bgColor: '#e0e0e0',
+ lightBgColor: '#003831',
+ introTextColor: 'rgba(92, 92, 92, 0.87)',
+ textColor: '#000000',
+ paragraphColor: '#777777',
+ linkColor: colors.brandDark,
+ mobileNavBgUpper: '#FFFFFF',
+ mobileNavBgLower: '#F3F6F4',
+ mobileNavColor: '#000000',
+ dropdownBg: '#FFFFFF',
+ dropdownButtonBg: '#F3F6F4',
+ dropdownColor: '#003831',
+ headerButtonBg: '#003831',
+ footerBg: '#181818',
+ footerColor: '#FFFFFF',
+ },
+};
+
+export class SiteWrap extends React.Component<Props, State> {
+ public state = {
+ isMobileNavOpen: false,
+ };
+
+ public componentDidMount(): void {
+ document.documentElement.style.overflowY = 'auto';
+ window.scrollTo(0, 0);
+ }
+
+ public toggleMobileNav = () => {
+ this.setState({
+ isMobileNavOpen: !this.state.isMobileNavOpen,
+ });
+ };
+
+ public render(): React.ReactNode {
+ const { children, theme = 'dark' } = this.props;
+ const { isMobileNavOpen } = this.state;
+ const currentTheme = GLOBAL_THEMES[theme];
+
+ return (
+ <>
+ <ThemeProvider theme={currentTheme}>
+ <>
+ <GlobalStyles />
+
+ <Header isNavToggled={isMobileNavOpen} toggleMobileNav={this.toggleMobileNav} />
+
+ <Main isNavToggled={isMobileNavOpen}>{children}</Main>
+
+ <Footer />
+ </>
+ </ThemeProvider>
+ </>
+ );
+ }
+}
+
+const Main =
+ styled.main <
+ MainProps >
+ `
+ transition: transform 0.5s, opacity 0.5s;
+ opacity: ${props => props.isNavToggled && '0.5'};
+`;
diff --git a/packages/website/ts/components/slider/slider.tsx b/packages/website/ts/components/slider/slider.tsx
new file mode 100644
index 000000000..33a352b9f
--- /dev/null
+++ b/packages/website/ts/components/slider/slider.tsx
@@ -0,0 +1,177 @@
+import * as React from 'react';
+import Flickity from 'react-flickity-component';
+import styled from 'styled-components';
+
+import { colors } from 'ts/style/colors';
+
+import { Icon } from 'ts/@next/components/icon';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+
+interface SliderProps {}
+
+interface SlideProps {
+ icon: string;
+ heading: string;
+ text: string;
+ href?: string;
+}
+
+const flickityOptions = {
+ initialIndex: 0,
+ cellAlign: 'left',
+ arrowShape:
+ 'M0 50.766L42.467 93.58l5.791-5.839-32.346-32.61H100V46.84H15.48L50.2 11.838 44.409 6 5.794 44.93l-.003-.003z',
+ prevNextButtons: true,
+};
+
+export const Slide: React.StatelessComponent<SlideProps> = (props: SlideProps) => {
+ const { heading, text, icon } = props;
+
+ return (
+ <StyledSlide>
+ <SlideHead>
+ <Icon name={icon} size="large" />
+ </SlideHead>
+ <SlideContent>
+ <Heading asElement="h4" size="small" marginBottom="15px">
+ {heading}
+ </Heading>
+ <Paragraph isMuted={true}>{text}</Paragraph>
+ </SlideContent>
+ </StyledSlide>
+ );
+};
+
+export const Slider: React.StatelessComponent<SliderProps> = props => {
+ return (
+ <StyledSlider>
+ <Flickity
+ className={'carousel'} // default ''
+ elementType={'div'} // default 'div'
+ options={flickityOptions} // takes flickity options {}
+ disableImagesLoaded={false} // default false
+ >
+ {props.children}
+ </Flickity>
+ </StyledSlider>
+ );
+};
+
+const StyledSlider = styled.div`
+ //overflow: hidden;
+ width: 100%;
+ height: 520px;
+
+ @media (max-width: 500px) {
+ height: 450px;
+ }
+
+ .carousel {
+ display: block;
+ user-select: none;
+ touch-action: pan-y;
+ -webkit-tap-highlight-color: transparent;
+ outline: none;
+
+ @media (max-width: 500px) {
+ overflow: hidden;
+ margin-left: -20px;
+ width: calc(100vw - 20px);
+ }
+ }
+
+ .flickity-viewport {
+ outline: none;
+ }
+
+ .flickity-button {
+ cursor: pointer;
+ position: absolute;
+ width: 74px;
+ height: 74px;
+ background-color: #000;
+ display: flex;
+ outline: 0;
+ top: calc(50% - 37px);
+ border: 0;
+ padding: 0;
+ transition: background-color 0.4s ease-in-out, visibility 0.4s ease-in-out, opacity 0.4s ease-in-out;
+
+ &:disabled {
+ opacity: 0;
+ visibility: hidden;
+ }
+
+ &:hover {
+ background-color: hsla(0, 0%, 10%, 1);
+ }
+
+ &.previous {
+ left: 0;
+ }
+
+ &.next {
+ right: 0;
+ }
+
+ svg {
+ margin: auto;
+ width: 28px;
+ height: auto;
+ }
+
+ path {
+ fill: #fff;
+ }
+ }
+`;
+
+const StyledSlide = styled.div`
+ background-color: ${colors.backgroundDark};
+ width: 560px;
+ height: 520px;
+ flex: 0 0 auto;
+ opacity: 0.3;
+ transition: opacity 0.4s ease-in-out;
+
+ & + & {
+ margin-left: 30px;
+ }
+
+ @media (max-width: 1200px) {
+ width: 100%;
+ }
+
+ @media (max-width: 500px) {
+ width: calc(100vw - 10px - 30px);
+ height: 450px;
+
+ & + & {
+ margin-left: 10px;
+ }
+ }
+
+ &.is-selected {
+ opacity: 1;
+ }
+`;
+
+const SlideHead = styled.div`
+ background-color: ${colors.brandDark};
+ height: 300px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @media (max-width: 500px) {
+ height: 240px;
+ }
+`;
+
+const SlideContent = styled.div`
+ padding: 30px;
+
+ @media (max-width: 500px) {
+ padding: 20px;
+ }
+`;
diff --git a/packages/website/ts/components/text.tsx b/packages/website/ts/components/text.tsx
new file mode 100644
index 000000000..9f6ed9e7a
--- /dev/null
+++ b/packages/website/ts/components/text.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import styled from 'styled-components';
+import { getCSSPadding, PaddingInterface } from 'ts/@next/constants/utilities';
+
+interface BaseTextInterface extends PaddingInterface {
+ size?: 'default' | 'medium' | 'large' | 'small' | number;
+ isCentered?: boolean;
+ textAlign?: string;
+}
+
+interface HeadingProps extends BaseTextInterface {
+ asElement?: 'h1' | 'h2' | 'h3' | 'h4';
+ maxWidth?: string;
+ fontWeight?: string;
+ isCentered?: boolean;
+ isFlex?: boolean;
+ isNoMargin?: boolean;
+ isMuted?: boolean | number;
+ marginBottom?: string;
+ color?: string;
+}
+
+interface ParagraphProps extends BaseTextInterface {
+ isNoMargin?: boolean;
+ marginBottom?: string; // maybe we should remove isNoMargin
+ isMuted?: boolean | number;
+ fontWeight?: string | number;
+}
+
+const StyledHeading =
+ styled.h1 <
+ HeadingProps >
+ `
+ max-width: ${props => props.maxWidth};
+ color: ${props => props.color || props.theme.textColor};
+ display: ${props => props.isFlex && `inline-flex`};
+ align-items: center;
+ justify-content: ${props => props.isFlex && `space-between`};
+ font-size: ${props =>
+ typeof props.size === 'string' ? `var(--${props.size || 'default'}Heading)` : `${props.size}px`};
+ line-height: ${props => `var(--${props.size || 'default'}HeadingHeight)`};
+ text-align: ${props => props.isCentered && 'center'};
+ padding: ${props => props.padding && getCSSPadding(props.padding)};
+ margin-left: ${props => props.isCentered && 'auto'};
+ margin-right: ${props => props.isCentered && 'auto'};
+ margin-bottom: ${props => !props.isNoMargin && (props.marginBottom || '30px')};
+ opacity: ${props => (typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted)};
+ font-weight: ${props => (props.fontWeight ? props.fontWeight : ['h4'].includes(props.asElement) ? 400 : 300)};
+ width: ${props => props.isFlex && `100%`};
+`;
+
+export const Heading: React.StatelessComponent<HeadingProps> = props => {
+ const { asElement = 'h1', children } = props;
+ const Component = StyledHeading.withComponent(asElement);
+
+ return <Component {...props}>{children}</Component>;
+};
+
+Heading.defaultProps = {
+ size: 'default',
+};
+
+// No need to declare it twice as Styled then rewrap as a stateless comp
+// Note: this would be useful to be implemented the same way was "Heading"
+// and be more generic. e.g. <Text /> with a props asElement so we can use it
+// for literally anything =
+export const Paragraph =
+ styled.p <
+ ParagraphProps >
+ `
+ font-size: ${props => `var(--${props.size || 'default'}Paragraph)`};
+ font-weight: ${props => props.fontWeight || 300};
+ margin-bottom: ${props => !props.isNoMargin && (props.marginBottom || '30px')};
+ padding: ${props => props.padding && getCSSPadding(props.padding)};
+ color: ${props => props.color || props.theme.paragraphColor};
+ opacity: ${props => (typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted)};
+ text-align: ${props => (props.textAlign ? props.textAlign : props.isCentered && 'center')};
+ line-height: 1.4;
+`;
+
+Paragraph.defaultProps = {
+ isMuted: true,
+};