Merge pull request #652 from 0xProject/feature/website/landing-subscribe-button-2

Implement subscribe form on landing page
This commit is contained in:
Francesco Agosti
2018-06-05 13:16:10 -07:00
committed by GitHub
30 changed files with 443 additions and 64 deletions

View File

@@ -0,0 +1,124 @@
import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { Button } from 'ts/components/ui/button';
import { Container } from 'ts/components/ui/container';
import { Input } from 'ts/components/ui/input';
import { Text } from 'ts/components/ui/text';
import { styled } from 'ts/style/theme';
import { backendClient } from 'ts/utils/backend_client';
export interface SubscribeFormProps {}
export enum SubscribeFormStatus {
None,
Error,
Success,
Loading,
Other,
}
export interface SubscribeFormState {
emailText: string;
lastSubmittedEmail: string;
status: SubscribeFormStatus;
}
const FORM_FONT_SIZE = '15px';
// TODO: Translate visible strings. https://app.asana.com/0/628666249318202/697485674422001
export class SubscribeForm extends React.Component<SubscribeFormProps, SubscribeFormState> {
public state = {
emailText: '',
lastSubmittedEmail: '',
status: SubscribeFormStatus.None,
};
public render(): React.ReactNode {
return (
<Container className="flex flex-column items-center justify-between md-mx2 sm-mx2">
<Container marginBottom="15px">
<Text fontFamily="Roboto Mono" fontColor={colors.grey} center={true}>
Subscribe to our newsletter for 0x relayer and dApp updates
</Text>
</Container>
<form onSubmit={this._handleFormSubmitAsync.bind(this)}>
<Container className="flex flex-wrap justify-center items-center">
<Container marginTop="15px">
<Input
placeholder="you@email.com"
value={this.state.emailText}
fontColor={colors.white}
fontSize={FORM_FONT_SIZE}
backgroundColor={colors.projectsGrey}
width="300px"
onChange={this._handleEmailInputChange.bind(this)}
/>
</Container>
<Container marginLeft="15px" marginTop="15px">
<Button
type="submit"
backgroundColor={colors.darkestGrey}
fontColor={colors.white}
fontSize={FORM_FONT_SIZE}
>
Subscribe
</Button>
</Container>
</Container>
</form>
{this._renderMessage()}
</Container>
);
}
private _renderMessage(): React.ReactNode {
let message = null;
switch (this.state.status) {
case SubscribeFormStatus.Error:
message = 'Sorry, something went wrong. Try again later.';
break;
case SubscribeFormStatus.Loading:
message = 'One second...';
break;
case SubscribeFormStatus.Success:
message = `Thanks! ${this.state.lastSubmittedEmail} is now on the mailing list.`;
break;
case SubscribeFormStatus.None:
break;
default:
throw new Error(
'The SubscribeFormStatus switch statement is not exhaustive when choosing an error message.',
);
}
return (
<Container isHidden={!message} marginTop="30px">
<Text center={true} fontFamily="Roboto Mono" fontColor={colors.grey}>
{message || 'spacer text (never shown to user)'}
</Text>
</Container>
);
}
private _handleEmailInputChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ emailText: event.target.value });
}
private async _handleFormSubmitAsync(event: React.FormEvent<HTMLInputElement>): Promise<void> {
event.preventDefault();
if (_.isUndefined(this.state.emailText) || _.isEmpty(this.state.emailText)) {
return;
}
this.setState({
status: SubscribeFormStatus.Loading,
lastSubmittedEmail: this.state.emailText,
});
try {
const response = await backendClient.subscribeToNewsletterAsync(this.state.emailText);
const status = response.status === 200 ? SubscribeFormStatus.Success : SubscribeFormStatus.Error;
this.setState({ status, emailText: '' });
} catch (error) {
this._setStatus(SubscribeFormStatus.Error);
}
}
private _setStatus(status: SubscribeFormStatus): void {
this.setState({ status });
}
}

View File

@@ -5,9 +5,9 @@ import Toggle from 'material-ui/Toggle';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { BalanceErrs, Token, TokenState } from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';

View File

@@ -5,7 +5,7 @@ import { Placement, Popper, PopperChildrenProps } from 'react-popper';
import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboarding/onboarding_tooltip';
import { Container } from 'ts/components/ui/container';
import { Overlay } from 'ts/components/ui/overlay';
import { zIndex } from 'ts/utils/style';
import { zIndex } from 'ts/style/z_index';
export interface Step {
target: string;

View File

@@ -2,7 +2,7 @@ import { Styles } from '@0xproject/react-shared';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { colors } from 'ts/utils/colors';
import { colors } from 'ts/style/colors';
export interface BackButtonProps {
to: string;

View File

@@ -4,8 +4,8 @@ import * as React from 'react';
import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu';
import { Identicon } from 'ts/components/ui/identicon';
import { colors } from 'ts/style/colors';
import { WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 45;

View File

@@ -2,8 +2,8 @@ import { Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { MenuItem } from 'ts/components/ui/menu_item';
import { colors } from 'ts/style/colors';
import { Environments, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
export interface MenuTheme {

View File

@@ -7,8 +7,8 @@ import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens';
import { Container } from 'ts/components/ui/container';
import { Island } from 'ts/components/ui/island';
import { TokenIcon } from 'ts/components/ui/token_icon';
import { colors } from 'ts/style/colors';
import { Token, WebsiteBackendRelayerInfo } from 'ts/types';
import { colors } from 'ts/utils/colors';
export interface RelayerGridTileProps {
relayerInfo: WebsiteBackendRelayerInfo;

View File

@@ -6,9 +6,9 @@ import { GridList } from 'material-ui/GridList';
import * as React from 'react';
import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile';
import { colors } from 'ts/style/colors';
import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types';
import { backendClient } from 'ts/utils/backend_client';
import { colors } from 'ts/utils/colors';
export interface RelayerIndexProps {
networkId: number;

View File

@@ -77,11 +77,11 @@ interface TokenBalancesProps {
interface TokenBalancesState {
errorType: BalanceErrs;
trackedTokenStateByAddress: TokenStateByAddress;
isBalanceSpinnerVisible: boolean;
isZRXSpinnerVisible: boolean;
isTokenPickerOpen: boolean;
isAddingToken: boolean;
trackedTokenStateByAddress: TokenStateByAddress;
}
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {

View File

@@ -9,10 +9,10 @@ import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { zIndex } from 'ts/style/z_index';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { zIndex } from 'ts/utils/style';
import { utils } from 'ts/utils/utils';
const ROOT_HEIGHT = 24;

View File

@@ -16,9 +16,9 @@ import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { zIndex } from 'ts/style/z_index';
import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { zIndex } from 'ts/utils/style';
import { Translate } from 'ts/utils/translate';
import { utils } from 'ts/utils/utils';

View File

@@ -0,0 +1,79 @@
import { colors } from '@0xproject/react-shared';
import { darken } from 'polished';
import * as React from 'react';
import { styled } from 'ts/style/theme';
export interface ButtonProps {
className?: string;
fontSize?: string;
fontColor?: string;
backgroundColor?: string;
borderColor?: string;
width?: string;
type?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
}
const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, onClick, type, className }) => (
<button type={type} className={className} onClick={onClick}>
{children}
</button>
);
export const Button = styled(PlainButton)`
cursor: pointer;
font-size: ${props => props.fontSize};
color: ${props => props.fontColor};
padding: 0.8em 2.2em;
border-radius: 6px;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
font-weight: 500;
font-family: 'Roboto';
width: ${props => props.width};
background-color: ${props => props.backgroundColor};
border: ${props => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')};
&:hover {
background-color: ${props => darken(0.1, props.backgroundColor)};
}
&:active {
background-color: ${props => darken(0.2, props.backgroundColor)};
}
`;
Button.defaultProps = {
fontSize: '12px',
backgroundColor: colors.white,
width: 'auto',
};
Button.displayName = 'Button';
type CallToActionType = 'light' | 'dark';
export interface CallToActionProps {
type?: CallToActionType;
fontSize?: string;
width?: string;
}
export const CallToAction: React.StatelessComponent<CallToActionProps> = ({ children, type, fontSize, width }) => {
const isLight = type === 'light';
const backgroundColor = isLight ? colors.white : colors.heroGrey;
const fontColor = isLight ? colors.heroGrey : colors.white;
const borderColor = isLight ? undefined : colors.white;
return (
<Button
fontSize={fontSize}
backgroundColor={backgroundColor}
fontColor={fontColor}
width={width}
borderColor={borderColor}
>
{children}
</Button>
);
};
CallToAction.defaultProps = {
type: 'dark',
};

View File

@@ -11,13 +11,20 @@ export interface ContainerProps {
paddingBottom?: StringOrNum;
paddingRight?: StringOrNum;
paddingLeft?: StringOrNum;
backgroundColor?: string;
borderRadius?: StringOrNum;
maxWidth?: StringOrNum;
children?: React.ReactNode;
isHidden?: boolean;
className?: string;
}
export const Container: React.StatelessComponent<ContainerProps> = (props: ContainerProps) => {
const { children, ...style } = props;
return <div style={style}>{children}</div>;
export const Container: React.StatelessComponent<ContainerProps> = ({ children, className, isHidden, ...style }) => {
const visibility = isHidden ? 'hidden' : undefined;
return (
<div style={{ ...style, visibility }} className={className}>
{children}
</div>
);
};
Container.displayName = 'Container';

View File

@@ -0,0 +1,43 @@
import { colors } from '@0xproject/react-shared';
import * as React from 'react';
import { styled } from 'ts/style/theme';
export interface InputProps {
className?: string;
value?: string;
width?: string;
fontSize?: string;
fontColor?: string;
placeholderColor?: string;
placeholder?: string;
backgroundColor?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => (
<input className={className} value={value} onChange={onChange} placeholder={placeholder} />
);
export const Input = styled(PlainInput)`
font-size: ${props => props.fontSize};
width: ${props => props.width};
padding: 0.8em 1.2em;
border-radius: 3px;
font-family: 'Roboto Mono';
color: ${props => props.fontColor};
border: none;
background-color: ${props => props.backgroundColor};
&::placeholder {
color: ${props => props.placeholderColor};
}
`;
Input.defaultProps = {
width: 'auto',
backgroundColor: colors.white,
fontColor: colors.darkestGrey,
placeholderColor: colors.darkGrey,
fontSize: '12px',
};
Input.displayName = 'Input';

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { colors } from 'ts/style/colors';
import { Styleable } from 'ts/types';
import { colors } from 'ts/utils/colors';
export interface IslandProps {
style?: React.CSSProperties;

View File

@@ -2,7 +2,7 @@ import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { zIndex } from 'ts/utils/style';
import { zIndex } from 'ts/style/z_index';
export interface OverlayProps {
children?: React.ReactNode;

View File

@@ -0,0 +1,56 @@
import { colors } from '@0xproject/react-shared';
import * as React from 'react';
import { styled } from 'ts/style/theme';
import { Deco, Key } from 'ts/types';
import { Translate } from 'ts/utils/translate';
export type TextTag = 'p' | 'div' | 'span' | 'label';
export interface TextProps {
className?: string;
Tag?: TextTag;
fontSize?: string;
fontFamily?: string;
fontColor?: string;
lineHeight?: string;
center?: boolean;
fontWeight?: number;
}
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, Tag }) => (
<Tag className={className}>{children}</Tag>
);
export const Text = styled(PlainText)`
font-family: ${props => props.fontFamily};
font-weight: ${props => props.fontWeight};
font-size: ${props => props.fontSize};
${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
${props => (props.center ? 'text-align: center' : '')};
color: ${props => props.fontColor};
`;
Text.defaultProps = {
fontFamily: 'Roboto',
fontWeight: 400,
fontColor: colors.white,
fontSize: '14px',
Tag: 'div',
};
Text.displayName = 'Text';
interface TranslatedProps {
children: Key;
translate: Translate;
deco?: Deco;
}
export type TranslatedTextProps = TextProps & TranslatedProps;
export const TranslatedText: React.StatelessComponent<TranslatedTextProps> = ({
translate,
children,
deco,
...textProps,
}) => <Text {...textProps}>{translate.get(children, deco)}</Text>;

View File

@@ -32,6 +32,8 @@ import { TokenIcon } from 'ts/components/ui/token_icon';
import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item';
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { zIndex } from 'ts/style/z_index';
import {
BalanceErrs,
BlockchainErrs,
@@ -46,9 +48,7 @@ import {
WebsitePaths,
} from 'ts/types';
import { backendClient } from 'ts/utils/backend_client';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { zIndex } from 'ts/utils/style';
import { utils } from 'ts/utils/utils';
import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';

View File

@@ -3,8 +3,8 @@ import FlatButton from 'material-ui/FlatButton';
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
import * as React from 'react';
import { colors } from 'ts/style/colors';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';

View File

@@ -10,8 +10,8 @@ import { Blockchain } from 'ts/blockchain';
import { EthAmountInput } from 'ts/components/inputs/eth_amount_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { BlockchainCallErrs, Side, Token } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';

View File

@@ -1,11 +1,13 @@
import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
import RaisedButton from 'material-ui/RaisedButton';
import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { Link } from 'react-router-dom';
import { Footer } from 'ts/components/footer';
import { SubscribeForm } from 'ts/components/forms/subscribe_form';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { CallToAction } from 'ts/components/ui/button';
import { Container } from 'ts/components/ui/container';
import { Dispatcher } from 'ts/redux/dispatcher';
import { Deco, Key, Language, ScreenWidths, WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
@@ -236,7 +238,7 @@ export class Landing extends React.Component<LandingProps, LandingState> {
<div className="clearfix py4" style={{ backgroundColor: colors.heroGrey }}>
<div className="mx-auto max-width-4 clearfix">
{this._renderWhatsNew()}
<div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-my4 md-my4 sm-mt2 sm-mb4 clearfix">
<div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-mt4 md-mt4 sm-mt2 sm-mb4 clearfix">
<div className="col lg-col-5 md-col-5 col-12 sm-center">
<img src="/images/landing/hero_chip_image.png" height={isSmallScreen ? 300 : 395} />
</div>
@@ -268,40 +270,31 @@ export class Landing extends React.Component<LandingProps, LandingState> {
>
{this.props.translate.get(Key.TopTagline)}
</div>
<div className="pt3 clearfix sm-mx-auto" style={{ maxWidth: 389 }}>
<div className="lg-pr2 md-pr2 col col-6 sm-center">
<Container className="pt3 clearfix sm-mx-auto" maxWidth="390px">
<div className="lg-pr2 md-pr2 lg-col lg-col-6 sm-center sm-col sm-col-12 mb2">
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
<RaisedButton
style={{ borderRadius: 6, minWidth: 157.36 }}
buttonStyle={{ borderRadius: 6 }}
labelStyle={buttonLabelStyle}
label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
onClick={_.noop}
/>
<CallToAction width="175px" type="light">
{this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
</CallToAction>
</Link>
</div>
<div className="col col-6 sm-center">
<div className="lg-col lg-col-6 sm-center sm-col sm-col-12">
<a
href={constants.URL_ZEROEX_CHAT}
target="_blank"
className="text-decoration-none"
>
<RaisedButton
style={{ borderRadius: 6, minWidth: 150 }}
buttonStyle={lightButtonStyle}
labelColor="white"
backgroundColor={colors.heroGrey}
labelStyle={buttonLabelStyle}
label={this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)}
onClick={_.noop}
/>
<CallToAction width="175px">
{this.props.translate.get(Key.CommunityCallToAction, Deco.Cap)}
</CallToAction>
</a>
</div>
</div>
</Container>
</div>
</div>
</div>
</div>
{this.props.translate.getLanguage() === Language.English && <SubscribeForm />}
</div>
);
}
@@ -782,15 +775,9 @@ export class Landing extends React.Component<LandingProps, LandingState> {
</div>
<div className="sm-center sm-pt2 lg-table-cell md-table-cell">
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
<RaisedButton
style={{ borderRadius: 6, minWidth: 150 }}
buttonStyle={lightButtonStyle}
labelColor={colors.white}
backgroundColor={colors.heroGrey}
labelStyle={buttonLabelStyle}
label={this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
onClick={_.noop}
/>
<CallToAction fontSize="15px">
{this.props.translate.get(Key.BuildCallToAction, Deco.Cap)}
</CallToAction>
</Link>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import * as styledComponents from 'styled-components';
const {
default: styled,
css,
injectGlobal,
keyframes,
ThemeProvider,
} = styledComponents as styledComponents.ThemedStyledComponentsModule<IThemeInterface>;
export interface IThemeInterface {}
export const theme = {};
export { styled, css, injectGlobal, keyframes, ThemeProvider };

View File

@@ -8,6 +8,7 @@ const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station';
const PRICES_ENDPOINT = '/prices';
const RELAYERS_ENDPOINT = '/relayers';
const WIKI_ENDPOINT = '/wiki';
const SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT = '/newsletter_subscriber/substack';
export const backendClient = {
async getGasInfoAsync(): Promise<WebsiteBackendGasInfo> {
@@ -33,4 +34,11 @@ export const backendClient = {
const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), WIKI_ENDPOINT);
return result;
},
async subscribeToNewsletterAsync(email: string): Promise<Response> {
const result = await fetchUtils.postAsync(utils.getBackendBaseUrl(), SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT, {
email,
referrer: window.location.href,
});
return result;
},
};

View File

@@ -4,22 +4,38 @@ import * as queryString from 'query-string';
import { errorReporter } from 'ts/utils/error_reporter';
const logErrorIfPresent = (response: Response, requestedURL: string) => {
if (response.status !== 200) {
const errorText = `Error requesting url: ${requestedURL}, ${response.status}: ${response.statusText}`;
logUtils.log(errorText);
const error = Error(errorText);
// tslint:disable-next-line:no-floating-promises
errorReporter.reportAsync(error);
throw error;
}
};
export const fetchUtils = {
async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> {
const query = queryStringFromQueryParams(queryParams);
const url = `${baseUrl}${path}${query}`;
const response = await fetch(url);
if (response.status !== 200) {
const errorText = `Error requesting url: ${url}, ${response.status}: ${response.statusText}`;
logUtils.log(errorText);
const error = Error(errorText);
// tslint:disable-next-line:no-floating-promises
errorReporter.reportAsync(error);
throw error;
}
logErrorIfPresent(response, url);
const result = await response.json();
return result;
},
async postAsync(baseUrl: string, path: string, body: object): Promise<Response> {
const url = `${baseUrl}${path}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
logErrorIfPresent(response, url);
return response;
},
};
function queryStringFromQueryParams(queryParams?: object): string {

View File

@@ -1,6 +1,6 @@
import { Styles } from '@0xproject/react-shared';
import { colors } from 'ts/utils/colors';
import { colors } from 'ts/style/colors';
export const styles: Styles = {
focusedItem: {