From abdf91c691b924b75d71db49fba296da9c8ddcac Mon Sep 17 00:00:00 2001 From: fragosti Date: Thu, 20 Dec 2018 16:01:53 -0800 Subject: feat: move all @next files to non @next directory --- packages/website/ts/components/aboutPageLayout.tsx | 71 ++++ .../website/ts/components/animatedChatIcon.tsx | 106 ++++++ .../website/ts/components/animatedCompassIcon.tsx | 53 +++ packages/website/ts/components/banner.tsx | 147 ++++++++ packages/website/ts/components/blockIconLink.tsx | 84 +++++ packages/website/ts/components/button.tsx | 102 ++++++ packages/website/ts/components/chapter_link.tsx | 15 + packages/website/ts/components/definition.tsx | 145 ++++++++ .../components/dropdowns/dropdown_developers.tsx | 180 ++++++++++ .../ts/components/dropdowns/dropdown_products.tsx | 48 +++ packages/website/ts/components/footer.tsx | 368 +++++++++------------ packages/website/ts/components/hamburger.tsx | 72 ++++ packages/website/ts/components/header.tsx | 252 ++++++++++++++ packages/website/ts/components/hero.tsx | 152 +++++++++ packages/website/ts/components/heroAnimation.tsx | 123 +++++++ packages/website/ts/components/heroImage.tsx | 27 ++ packages/website/ts/components/icon.tsx | 72 ++++ packages/website/ts/components/image.tsx | 20 ++ packages/website/ts/components/layout.tsx | 177 ++++++++++ packages/website/ts/components/link.tsx | 64 ++++ packages/website/ts/components/logo.tsx | 41 +++ packages/website/ts/components/mobileNav.tsx | 121 +++++++ packages/website/ts/components/modals/input.tsx | 95 ++++++ .../website/ts/components/modals/modal_contact.tsx | 278 ++++++++++++++++ packages/website/ts/components/newLayout.tsx | 152 +++++++++ packages/website/ts/components/newsletter_form.tsx | 202 +++++++++++ .../ts/components/sections/landing/about.tsx | 81 +++++ .../ts/components/sections/landing/clients.tsx | 113 +++++++ .../website/ts/components/sections/landing/cta.tsx | 29 ++ .../ts/components/sections/landing/hero.tsx | 31 ++ packages/website/ts/components/separator.tsx | 7 + packages/website/ts/components/siteWrap.tsx | 149 +++++++++ packages/website/ts/components/slider/slider.tsx | 177 ++++++++++ packages/website/ts/components/text.tsx | 83 +++++ 34 files changed, 3623 insertions(+), 214 deletions(-) create mode 100644 packages/website/ts/components/aboutPageLayout.tsx create mode 100644 packages/website/ts/components/animatedChatIcon.tsx create mode 100644 packages/website/ts/components/animatedCompassIcon.tsx create mode 100644 packages/website/ts/components/banner.tsx create mode 100644 packages/website/ts/components/blockIconLink.tsx create mode 100644 packages/website/ts/components/button.tsx create mode 100644 packages/website/ts/components/chapter_link.tsx create mode 100644 packages/website/ts/components/definition.tsx create mode 100644 packages/website/ts/components/dropdowns/dropdown_developers.tsx create mode 100644 packages/website/ts/components/dropdowns/dropdown_products.tsx create mode 100644 packages/website/ts/components/hamburger.tsx create mode 100644 packages/website/ts/components/header.tsx create mode 100644 packages/website/ts/components/hero.tsx create mode 100644 packages/website/ts/components/heroAnimation.tsx create mode 100644 packages/website/ts/components/heroImage.tsx create mode 100644 packages/website/ts/components/icon.tsx create mode 100644 packages/website/ts/components/image.tsx create mode 100644 packages/website/ts/components/layout.tsx create mode 100644 packages/website/ts/components/link.tsx create mode 100644 packages/website/ts/components/logo.tsx create mode 100644 packages/website/ts/components/mobileNav.tsx create mode 100644 packages/website/ts/components/modals/input.tsx create mode 100644 packages/website/ts/components/modals/modal_contact.tsx create mode 100644 packages/website/ts/components/newLayout.tsx create mode 100644 packages/website/ts/components/newsletter_form.tsx create mode 100644 packages/website/ts/components/sections/landing/about.tsx create mode 100644 packages/website/ts/components/sections/landing/clients.tsx create mode 100644 packages/website/ts/components/sections/landing/cta.tsx create mode 100644 packages/website/ts/components/sections/landing/hero.tsx create mode 100644 packages/website/ts/components/separator.tsx create mode 100644 packages/website/ts/components/siteWrap.tsx create mode 100644 packages/website/ts/components/slider/slider.tsx create mode 100644 packages/website/ts/components/text.tsx (limited to 'packages/website/ts/components') 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) => ( + +
+ + Mission + Team + Press + Jobs + + + + + {props.title} + + + {props.description} + + + {props.linkLabel && + (props.href || props.to) && ( + + {props.linkLabel} + + )} + + +
+ + {props.children} +
+); + +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 = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +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 = () => ( + + + + + + + + + + + + + + + + +); + +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) => { + const { heading, subline, mainCta, secondaryCta } = props; + return ( + + + + + + {heading} + + {subline && ( + + {subline} + + )} + + + + {mainCta && ( + + )} + + {secondaryCta && ( + + )} + + + + ); +}; + +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; +} + +class BaseComponent extends React.PureComponent { + 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 ( + +
+ + + {title} + + +
+
+ ); + } +} + +export const BlockIconLink = withRouter(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(linkElem) : ButtonBase; + const targetProp = href && target ? { target } : {}; + + return ( + + {children} + + {isWithArrow && ( + + + + )} + + ); +}; + +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) => ( + + + + + + {props.title} + + + {typeof props.description === 'string' ? ( + + {props.description} + + ) : ( + <>{props.description} + )} + + {props.actions && ( + + {props.actions.map((item, index) => ( + + ))} + + )} + + +); + +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 = withTheme((props: Props) => ( + <> + +
+ + Getting Started + + + + {_.map(introData, (item, index) => ( +
  • + + {item.label} + +
  • + ))} +
    +
    + + + + + Popular Docs + + +
      + {_.map(docsData, (item, index) => ( +
    • + + {item.label} + +
    • + ))} +
    +
    + + + + Useful Links + + +
      + {_.map(linksData, (item, index) => ( +
    • + + {item.label} + +
    • + ))} +
    +
    +
    + + + View All Documentation + +
    + +)); + +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<{}> = () => ( + + {_.map(navData, (item, index) => ( +
  • + + + {item.title} + + + {item.description && ( + + {item.description} + + )} + +
  • + ))} +
    +); + +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 { - 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 = () => ( + + + + + + + + + + {_.map(linkRows, (row: LinkRows, index) => ( + + + {row.heading} + + + + + ))} + + + + +); + +const LinkList = (props: LinkListProps) => ( + + {_.map(props.links, (link, index) => ( +
  • + + {link.text} + +
  • + ))} +
    +); + +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 = { - [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 ; - }); - return ( -
    -
    -
    -
    -
    - -
    -
    - © ZeroEx, Intl. -
    -
    - - {languageMenuItems} - -
    -
    -
    -
    -
    -
    - {this._renderHeader(Key.Documentation)} - {_.map(sectionNameToLinks[Key.Documentation], this._renderMenuItem.bind(this))} -
    -
    -
    -
    - {this._renderHeader(Key.Community)} - {_.map(sectionNameToLinks[Key.Community], this._renderMenuItem.bind(this))} -
    -
    -
    -
    - {this._renderHeader(Key.Organization)} - {_.map(sectionNameToLinks[Key.Organization], this._renderMenuItem.bind(this))} -
    -
    -
    -
    -
    - ); +`; + +const FooterColumn = styled(Column)` + @media (min-width: 768px) { + width: ${props => props.width}; } - private _renderIcon(fileName: string): React.ReactNode { - return ( -
    - -
    - ); + + @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 ( -
    - -
    - {!_.isUndefined(iconIfExists) ? ( -
    -
    {this._renderIcon(iconIfExists)}
    -
    {link.title}
    -
    - ) : ( - link.title - )} -
    - -
    - ); +`; + +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 ( -
    - {this.props.translate.get(key, Deco.Upper)} -
    - ); +`; + +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) => { + return ( + + + + + + ); +}; + +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; + 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 { + public onUnpin = () => { + if (this.props.isNavToggled) { + this.props.toggleMobileNav(); + } + }; + + public render(): React.ReactNode { + const { isNavToggled, toggleMobileNav, theme } = this.props; + + return ( + + + + + + + + + {_.map(navItems, (link, index) => )} + + + + + Trade on 0x + + + + + + + + + ); + } +} + +export const Header = withTheme(HeaderBase); + +const NavItem = (props: { link: NavItemProps; key: string }) => { + const { link } = props; + const Subnav = link.dropdownComponent; + const linkElement = _.isUndefined(link.url) ? ( + {link.text} + ) : ( + + {link.text} + + ); + return ( + + {linkElement} + + {link.dropdownComponent && ( + + + + )} + + ); +}; + +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.figure && {props.figure}} + + + + {props.title} + + + {props.description} + + {props.actions && {props.actions}} + + +
    +); + +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 = () => ( + + + + + + + + + + + + + + +); + +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) => {props.image}; + +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 = (props: IconProps) => { + if (props.name && !props.component) { + const IconSVG = Loadable({ + loader: async () => import(/* webpackChunkName: "icon" */ `ts/@next/icons/illustrations/${props.name}.svg`), + loading: () => Loading, + }); + + return ( + + + + ); + } + + if (props.component) { + return {props.component}; + } + + 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) => { + return ; +}; + +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
    ? +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 ( + + {children} + {!isNoArrow && ( + + + + )} + + ); +}; + +// 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 = (props: LogoInterface) => ( + + + +); 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 { + public render(): React.ReactNode { + const { isToggled, toggleMobileNav } = this.props; + + return ( + + +
    +

    Products

    + +
      +
    • + 0x Instant +
    • +
    • + 0x Launch Kit +
    • +
    +
    + +
    + +
  • + Why 0x +
  • +
  • + About +
  • +
  • + + Blog + +
  • +
    +
    + + {isToggled && } +
    +
    + ); + } +} + +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) => { + 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 ( + + + + {isErrors && {errorMessage}} + + ); +}); + +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 { + public state = { + isSubmitting: false, + isSuccessful: false, + errors: {}, + }; + public nameRef: React.RefObject = React.createRef(); + public emailRef: React.RefObject = React.createRef(); + public companyProjectRef: React.RefObject = React.createRef(); + public linkRef: React.RefObject = React.createRef(); + public commentsRef: React.RefObject = React.createRef(); + public constructor(props: Props) { + super(props); + } + public render(): React.ReactNode { + const { isOpen, onDismiss } = this.props; + const { isSuccessful, errors } = this.state; + + return ( + <> + + +
    + + Contact the 0x Core Team + + + 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. + + + + + + + + + + + + + + + + + + +
    + + + + Thanks for contacting us. + + + We'll get back to you soon. If you need quick support in the meantime, reach out to the + 0x team on Discord. + + + +
    +
    + + ); + } + private async _onSubmitAsync(e: Event): Promise { + 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 = (props: SectionProps) => { + return ( + + {props.children} + + ); +}; + +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) => { + const { name, label, type } = props; + const id = `input-${name}`; + + return ( + + + + + ); +}); + +class Form extends React.Component { + public emailInput = React.createRef(); + public state = { + isSubmitted: false, + }; + public render(): React.ReactNode { + const { isSubmitted } = this.state; + const { theme } = this.props; + + return ( + + + + + + + + + + 🎉 Thank you for signing up! + + Subscribe to our newsletter for updates in the 0x ecosystem + + ); + } + + private async _onSubmitAsync(e: React.FormEvent): Promise { + 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 = () => ( +
    + + + + + + + + + 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. + + + + Discover how developers use 0x + + +
    + + +
    + +
    + +
    + +
    +); + +const Figure = (props: FigureProps) => ( + + {props.value} + {props.description} + +); + +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 = () => ( +
    + Join the growing number of projects developing on 0x + + + {_.map(projects, (item: ProjectLogo, index) => ( + + {item.name} + + ))} + +
    +); + +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) => ( +
    + } + title="Ready to build on 0x?" + linkLabel="Get Started" + linkUrl={WebsitePaths.Docs} + /> + } + title="Want help from the 0x team?" + linkLabel="Get in Touch" + linkAction={props.onContactClick} + /> +
    +); 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 = () => ( + } />} + actions={} + /> +); + +const HeroActions = () => ( + <> + + + + +); 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 { + 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 ( + <> + + <> + + +
    + +
    {children}
    + +