created most of the page's functionality

This commit is contained in:
David Sun
2019-03-06 10:19:06 -08:00
parent 28c4ca73ab
commit f72918362d
23 changed files with 587 additions and 2 deletions

BIN
packages/website/.DS_Store vendored Normal file

Binary file not shown.

BIN
packages/website/public/.DS_Store vendored Normal file

Binary file not shown.

BIN
packages/website/public/images/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -21,6 +21,7 @@ export interface ButtonInterface {
isAccentColor?: boolean;
hasIcon?: boolean | string;
isInline?: boolean;
padding?: string;
href?: string;
type?: string;
target?: string;
@@ -72,6 +73,7 @@ const ButtonBase = styled.button<ButtonInterface>`
border-color: ${props => props.isTransparent && !props.isWithArrow && props.borderColor};
color: ${props => (props.isAccentColor ? props.theme.linkColor : props.color || props.theme.textColor)};
padding: ${props => !props.isNoPadding && !props.isWithArrow && '18px 30px'};
padding: ${props => !!props.padding && props.padding};
white-space: ${props => props.isWithArrow && 'nowrap'};
text-align: center;
font-size: ${props => (props.isWithArrow ? '20px' : '18px')};

View File

@@ -93,8 +93,8 @@ class HeaderBase extends React.Component<HeaderProps> {
</NavLinks>
<MediaQuery minWidth={990}>
<TradeButton bgColor={theme.headerButtonBg} color="#ffffff" href="/portal">
Trade on 0x
<TradeButton bgColor={theme.headerButtonBg} color="#ffffff" href="/explore">
Explore 0x
</TradeButton>
</MediaQuery>

View File

@@ -94,6 +94,7 @@ const SectionBase = styled.section<SectionProps>`
max-width: 1500px;
margin: 0 auto;
padding: ${props => props.isPadded && '120px 0'};
padding: ${props => !!props.padding && props.padding};
background-color: ${props => props.theme[`${props.bgColor}BgColor`] || props.bgColor};
position: relative;
overflow: ${props => !props.isFullWidth && 'hidden'};

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import styled from 'styled-components';
import { Icon } from 'ts/components/icon';
interface InputProps {
className?: string;
name?: string;
width?: string;
type?: string;
defaultValue?: string;
placeholder?: string;
onChange?: (e: React.ChangeEvent) => void;
}
export const Input = React.forwardRef((props: InputProps, ref?: React.Ref<HTMLInputElement>) => {
const { name, type, placeholder, defaultValue, onChange, width, className } = props;
const componentType = type === 'textarea' ? 'textarea' : 'input';
const inputProps = { name, type };
return (
<InputWrapper className={className} width={width}>
<Icon size={20} name="search"/>
<StyledInput
as={componentType}
ref={ref}
id={`input-${name}`}
placeholder={placeholder}
defaultValue={defaultValue}
onChange={onChange}
{...inputProps}
/>
</InputWrapper>
);
});
const StyledInput = styled.input`
appearance: none;
border: none;
color: #000;
font-size: 1.294117647rem;
padding: 16px 15px 14px;
outline: none;
width: 100%;
min-height: ${props => props.type === 'textarea' && `120px`};
&::placeholder {
color: #c3c3c3;
}
`;
const InputWrapper = styled.div<InputProps>`
display: flex;
align-items: center;
position: relative;
width: ${props => props.width || '100%'};
border-bottom: 1px solid #d5d5d5;
@media (max-width: 768px) {
width: 100%;
margin-bottom: 30px;
}
`;

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="11.1228" y1="11.1849" x2="19.5844" y2="19.6464" stroke="black"/>
<circle cx="6.53846" cy="6.53846" r="6.03846" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,3 @@
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6842 8C17.6842 8.4 17.3817 8.70588 16.9862 8.70588H6.16621C5.88698 9.52941 5.09585 10.1176 4.18837 10.1176C3.28089 10.1176 2.51302 9.52941 2.21053 8.70588H0.698061C0.302493 8.70588 0 8.4 0 8C0 7.6 0.302493 7.29412 0.698061 7.29412H2.21053C2.48975 6.47059 3.28089 5.88235 4.18837 5.88235C5.09585 5.88235 5.86371 6.47059 6.16621 7.29412H16.9862C17.3817 7.29412 17.6842 7.6 17.6842 8ZM16.9862 13.1765H10.8199C10.5407 12.3529 9.74959 11.7647 8.84211 11.7647C7.93463 11.7647 7.16676 12.3529 6.86427 13.1765H0.698061C0.302493 13.1765 0 13.4824 0 13.8824C0 14.2824 0.302493 14.5882 0.698061 14.5882H6.86427C7.14349 15.4118 7.93463 16 8.84211 16C9.74959 16 10.5175 15.4118 10.8199 14.5882H16.9862C17.3817 14.5882 17.6842 14.2824 17.6842 13.8824C17.6842 13.4824 17.3817 13.1765 16.9862 13.1765ZM0.698061 2.82353H11.518C11.7972 3.64706 12.5884 4.23529 13.4958 4.23529C14.4033 4.23529 15.1712 3.64706 15.4737 2.82353H16.9862C17.3817 2.82353 17.6842 2.51765 17.6842 2.11765C17.6842 1.71765 17.3817 1.41176 16.9862 1.41176H15.4737C15.1945 0.588235 14.4033 0 13.4958 0C12.5884 0 11.8205 0.588235 11.518 1.41176H0.698061C0.302493 1.41176 0 1.71765 0 2.11765C0 2.51765 0.302493 2.82353 0.698061 2.82353Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -21,6 +21,8 @@ import { NextAboutMission } from 'ts/pages/about/mission';
import { NextAboutPress } from 'ts/pages/about/press';
import { NextAboutTeam } from 'ts/pages/about/team';
import { Credits } from 'ts/pages/credits';
import { Explore } from 'ts/pages/explore';
import { NextEcosystem } from 'ts/pages/ecosystem';
import { Extensions } from 'ts/pages/extensions';
import { Governance } from 'ts/pages/governance/governance';
@@ -113,6 +115,7 @@ render(
path={WebsitePaths.MarketMaker}
component={NextMarketMaker as any}
/>
<Route exact={true} path={WebsitePaths.Explore} component={Explore as any} />
<Route exact={true} path={WebsitePaths.Credits} component={Credits as any} />
<Route exact={true} path={WebsitePaths.Instant} component={Next0xInstant as any} />
<Route exact={true} path={WebsitePaths.LaunchKit} component={NextLaunchKit as any} />

View File

@@ -0,0 +1,300 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { Banner } from 'ts/components/banner';
import { DocumentTitle } from 'ts/components/document_title';
import { Icon } from 'ts/components/icon';
import { ModalContact, ModalContactType } from 'ts/components/modals/modal_contact';
import { Section } from 'ts/components/newLayout';
import { SiteWrap } from 'ts/components/siteWrap';
import { Heading } from 'ts/components/text';
import { Input as SearchInput } from 'ts/components/ui/search_textfield';
import { ExploreGrid } from 'ts/pages/explore/explore_grid';
import { Button as ExploreTagButton } from 'ts/pages/explore/explore_tag_button';
import { colors } from 'ts/style/colors';
import { ExploreEntry, ExploreEntryVisibility, ExploreFilterMetadata, ExploreFilterType, RicherExploreEntry } from 'ts/types';
import { documentConstants } from 'ts/utils/document_meta_constants';
export interface ExploreProps {}
const PROJECTS: { [s: string]: ExploreEntry } = {
paradex: {
label: 'Paradex',
description: 'Paradex is a matching relayer with a focus on stable coins that is now a part of Coinbase.',
logo_url: '/images/explore/paradex.png',
theme_color: '#151628',
url: 'https://paradex.io/',
keywords: ['relayer'],
instant: {
orderSource: '',
},
},
veil: {
label: 'Veil',
description: 'Veil is a non-custodial trading platform for blockchain-based derivatives and prediction markets.',
logo_url: '/images/explore/veil.png',
theme_color: '#0204EB',
url: 'https://veil.co/',
keywords: ['relayer'],
},
radar_relay: {
label: 'Radar Relay',
description: 'Radar Relay is an open order book relayer made by an international team based in Colorado.',
logo_url: '/images/explore/radar_relay.png',
theme_color: '#262626',
url: 'https://radarrelay.com/',
keywords: ['relayer'],
},
emoon: {
label: 'Emoon',
description: 'Emoon is a peer-to-peer marketplace for the exchange of ERC-20 and ERC-721 crypto assets.',
logo_url: '/images/explore/emoon.png',
theme_color: '#3F89E7',
url: 'https://www.emoon.io/',
keywords: ['relayer', 'collectibles'],
},
openrelay: {
label: 'OpenRelay',
description: 'Open Relay is an open order book relayer with a focus on scalable and open source backend infrastructure.',
logo_url: '/images/explore/open_relay.png',
theme_color: '#163AAB',
url: 'https://openrelay.xyz/',
keywords: ['relayer'],
},
boxswap: {
label: 'BoxSwap',
description: 'OTC relayer made for swapping ERC-20 and ERC-721 assets in a marketplace communities.',
logo_url: '/images/explore/box_swap.png',
theme_color: '#FF99DF',
url: 'https://boxswap.io/',
keywords: ['relayer', 'collectibles'],
},
};
const FILTERS: ExploreFilterMetadata[] = [{
label: 'All',
name: 'all',
filterType: ExploreFilterType.All,
}, {
label: 'Relayer',
name: 'relayer',
filterType: ExploreFilterType.Keyword,
keyword: 'relayer',
}, {
label: 'Collectibles',
name: 'collectibles',
filterType: ExploreFilterType.Keyword,
keyword: 'ERC-721',
}];
enum ExploreEntriesModifiers {
Filter = 'FILTER',
Search = 'SEARCH',
}
enum ExploreEntriesOrdering {
None = 'NONE',
}
export class Explore extends React.Component<ExploreProps> {
public state = {
isContactModalOpen: false,
entries: [] as RicherExploreEntry[],
entriesOrdering: ExploreEntriesOrdering.None,
filters: FILTERS,
query: '',
};
constructor(props: ExploreProps) {
super(props);
}
public componentWillMount(): void {
// tslint:disable-next-line:no-empty
this._loadEntriesAsync().then(() => {
this._setFilter('all');
});
}
public render(): React.ReactNode {
return (
<SiteWrap theme="light">
<DocumentTitle {...documentConstants.EXPLORE} />
<ExploreHero onSearch={this._changeSearchResults} />
<Section padding={'0 0 120px 0'} maxWidth={'1150px'}>
<ExploreToolBar onFilterClick={this._setFilter} filters={this.state.filters} />
<ExploreGrid entries={this.state.entries} />
</Section>
<Banner
heading="Have a 0x project?"
subline="Lorem Ipsum something then that and say something more."
mainCta={{ text: 'Apply Now', onClick: this._onOpenContactModal }}
secondaryCta={{ text: 'Join Discord', href: 'https://discordapp.com/invite/d3FTX3M' }}
/>
<ModalContact
isOpen={this.state.isContactModalOpen}
onDismiss={this._onDismissContactModal}
modalContactType={ModalContactType.Credits}
/>
</SiteWrap>
);
}
private readonly _onOpenContactModal = (): void => {
this.setState({ isContactModalOpen: true });
};
private readonly _onDismissContactModal = (): void => {
this.setState({ isContactModalOpen: false });
};
private _launchInstantAsync = async (): Promise<void> => {
};
private _changeSearchResults = (query: string): void => {
this.setState({ query: query.trim().toLowerCase() }, () => {
this._setEntriesModifier(ExploreEntriesModifiers.Search);
});
}
private _setFilter = (filterName: string, active: boolean = true): void => {
let updatedFilters: ExploreFilterMetadata[];
if (filterName === 'all') {
updatedFilters = this.state.filters.map(f => {
const newFilter = _.assign({}, f);
newFilter.active = newFilter.name === 'all' ? active : false;
return newFilter;
});
} else {
updatedFilters = this.state.filters.map(f => {
const newFilter = _.assign({}, f);
newFilter.active = newFilter.name === filterName ? active : newFilter.active;
newFilter.active = newFilter.name === 'all' ? false : newFilter.active;
return newFilter;
});
}
// If no filters are enabled, default to all
if (_.filter(updatedFilters, f => f.active).length === 0) {
this._setFilter('all');
} else {
this.setState({ filters: updatedFilters }, () => {
this._setEntriesModifier(ExploreEntriesModifiers.Filter);
});
}
};
private _setEntriesModifier = async (modifier: ExploreEntriesModifiers): Promise<void> => {
let newEntries: RicherExploreEntry[];
if (modifier === ExploreEntriesModifiers.Filter || modifier === ExploreEntriesModifiers.Search) {
const activeFilters = _.filter(this.state.filters, f => f.active);
if (activeFilters.length === 1 && activeFilters[0].name === 'all') {
newEntries = _.concat([], this.state.entries).map(e => {
const newEntry = _.assign({}, e);
newEntry.visibility = ExploreEntryVisibility.Visible;
if (modifier === ExploreEntriesModifiers.Search && newEntry.visibility === ExploreEntryVisibility.Visible) {
newEntry.visibility = (_.includes(newEntry.label.toLowerCase(), this.state.query) && ExploreEntryVisibility.Visible) || ExploreEntryVisibility.Hidden;
}
return newEntry;
});
} else {
newEntries = _.concat([], this.state.entries).map(e => {
const newEntry = _.assign({}, e);
newEntry.visibility = _.intersectionWith(activeFilters, newEntry.keywords, (f, k) => k === f.name).length !== 0 ? ExploreEntryVisibility.Visible : ExploreEntryVisibility.Hidden;
if (modifier === ExploreEntriesModifiers.Search && newEntry.visibility === ExploreEntryVisibility.Visible) {
newEntry.visibility = (_.includes(newEntry.label.toLowerCase(), this.state.query) && ExploreEntryVisibility.Visible) || ExploreEntryVisibility.Hidden;
}
return newEntry;
});
}
}
this.setState({ entries: newEntries});
};
// For future versions, ordering can be determined by async processes
private _setEntriesOrderingAsync = async (entries: RicherExploreEntry[]): Promise<RicherExploreEntry[]> => {
switch (this.state.entriesOrdering) {
default: return entries;
}
}
// For future versions, the load entries function can be async
private _loadEntriesAsync = async (): Promise<void> => {
const rawEntries = _.values(PROJECTS).map(e => _.assign(e, { visibility: ExploreEntryVisibility.Visible})) as RicherExploreEntry[];
const entries = await this._setEntriesOrderingAsync(rawEntries);
this.setState({ entries });
}
}
const ExploreHeroContentWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
interface ExploreHeroProps {
onSearch(query: string): void;
}
const ExploreHero = (props: ExploreHeroProps) => {
const onSearchDebounce = _.debounce(props.onSearch, 300);
const onChange = (e: any) => { onSearchDebounce(e.target.value); };
return <Section maxWidth={'1150px'}>
<ExploreHeroContentWrapper>
<Heading isNoMargin={true} size="large">Explore 0x</Heading>
<SearchInput onChange={onChange} width={'28rem'} placeholder="Search tokens, relayers, and dApps..."/>
</ExploreHeroContentWrapper>
</Section>;
};
const ExploreToolBarWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const ExploreToolBarContentWrapper = styled.div`
display: flex;
padding: 2rem 0;
& > * {
margin: 0 0.3rem;
}
& *:first-child {
margin-left: 0;
}
& *:last-child {
margin-right: 0;
}
`;
interface ExploreToolBarProps {
filters: ExploreFilterMetadata[];
onFilterClick(filterName: string, active: boolean): void;
}
const SettingsIconWrapper = styled.div`
padding-right: 0.4rem;
display: inline;
& > * {
transform: translateY(2px);
}
`;
const ExploreToolBar = (props: ExploreToolBarProps) => {
return <ExploreToolBarWrapper>
<ExploreToolBarContentWrapper>
{!!props.filters && props.filters.map(f => {
const onClick = () => { props.onFilterClick(f.name, !f.active); };
return <ExploreTagButton onClick={onClick} active={f.active} key={f.name}>{f.label}</ExploreTagButton>;
})}
</ExploreToolBarContentWrapper>
<ExploreToolBarContentWrapper>
<ExploreTagButton disableHover={true}>
<SettingsIconWrapper>
<Icon color={colors.grey} name="settings" size={16} />
</SettingsIconWrapper>
Featured
</ExploreTagButton>
</ExploreToolBarContentWrapper>
</ExploreToolBarWrapper>;
};

View File

@@ -0,0 +1,43 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { ExploreGridTile } from 'ts/pages/explore/explore_grid_tile';
import { ExploreEntryVisibility, RicherExploreEntry} from 'ts/types';
export interface ExptoreGridProps {
entries: RicherExploreEntry[];
}
export class ExploreGrid extends React.Component<ExptoreGridProps> {
constructor(props: ExptoreGridProps) {
super(props);
}
public render(): React.ReactNode {
return (
<ExploreGridList>
{this.props.entries.filter(e => e.visibility !== ExploreEntryVisibility.Hidden).map(e => {
return <ExploreGridTile {...e} key={e.label} />;
})}
</ExploreGridList>
);
}
}
interface ExploreGridListProps {
}
const ExploreGridList = styled.div<ExploreGridListProps>`
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 1.5rem;
grid-row-gap: 1.5rem;
@media (max-width: 56rem) {
grid-template-columns: repeat(2, 1fr);;
}
@media (max-width: 36rem) {
grid-template-columns: repeat(1, 1fr);;
}
`;

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import styled from 'styled-components';
import { Button } from 'ts/components/button';
import { Heading, Paragraph } from 'ts/components/text';
import { Image } from 'ts/components/ui/image';
import { RicherExploreEntry } from 'ts/types';
export const ExploreGridTile = (props: RicherExploreEntry) => {
return (<ExploreGridTileWrapper>
{!!props.instant && (<ExploreGridButtonWrapper>
<Button onClick={props.onInstantClick} padding={'12px 18px'} bgColor={'white'}>Instant Trade</Button>
</ExploreGridButtonWrapper>)}
<ExploreGridTileLink href={props.url} target="_blank">
<ExploreGridHeroWell color={props.theme_color}>
<Image
src={props.logo_url}
height={'90px'}
/>
</ExploreGridHeroWell>
<ExploreGridContentWell>
<Heading marginBottom={'0.5rem'} size={'small'}>{props.label}</Heading>
<Paragraph marginBottom={'0.5rem'}>{props.description}</Paragraph>
</ExploreGridContentWell>
</ExploreGridTileLink>
</ExploreGridTileWrapper>);
};
interface ExploreGridHeroWellProps {
color: string;
}
const ExploreGridHeroWell = styled.div<ExploreGridHeroWellProps>`
background-color: ${props => props.color};
height: 14rem;
display: flex;
align-items: center;
justify-content: center;
position: relative;
`;
const ExploreGridContentWell = styled.div`
padding: 1.5rem;
`;
const ExploreGridTileLink = styled.a`
display: block;
`;
const ExploreGridTileWrapper = styled.div`
display: block;
position: relative;
background-color: white;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
transition: box-shadow 200ms ease-in-out;
&:hover {
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.1);
}
`;
const ExploreGridButtonWrapper = styled.div`
position: absolute;
top: 1rem;
right: 1rem;
z-index: 1;
`;

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { Link as ReactRouterLink } from 'react-router-dom';
import styled from 'styled-components';
import { ThemeInterface } from 'ts/components/siteWrap';
import { colors } from 'ts/style/colors';
export interface ButtonInterface {
isDisabled?: boolean;
disableHover?: boolean;
className?: string;
active?: boolean;
children?: React.ReactNode | string;
hasIcon?: boolean | string;
padding?: string;
onClick?: (e: any) => any;
theme?: ThemeInterface;
}
export const Button: React.StatelessComponent<ButtonInterface> = (props: ButtonInterface) => {
const { children, isDisabled, className } = props;
const buttonProps = { disabled: isDisabled };
return (
<ButtonBase className={className} {...buttonProps} {...props}>
{children}
</ButtonBase>
);
};
Button.defaultProps = {
};
const ButtonBase = styled.button<ButtonInterface>`
appearance: none;
border: 1px solid transparent;
display: inline-block;
background-color: ${props => props.active ? colors.brandLight : 'transparent'};
border-color: ${props => props.active ? colors.brandLight : colors.grey};
color: ${props => props.active ? colors.white : colors.grey};
padding: 12px 18px;
padding: ${props => !!props.padding && props.padding};
text-align: center;
font-size: 18px;
text-decoration: none;
cursor: pointer;
outline: none;
transition: background-color 0.35s, border-color 0.35s, color 0.35s;
&:hover {
border-color: ${props => props.disableHover ? colors.grey : colors.brandLight};
color: ${props => props.disableHover ? colors.grey : props.active ? colors.white : colors.brandLight};
}
`;

View File

@@ -241,6 +241,45 @@ export enum Environments {
export type ContractInstance = any; // TODO: add type definition for Contract
export interface ExploreEntryInstantMetadata {
orderSource: string;
availableAssetDatas?: string;
}
export interface ExploreEntry {
label: string;
description: string;
logo_url: string;
theme_color: string;
url: string;
keywords: string[];
instant?: ExploreEntryInstantMetadata;
}
export enum ExploreEntryVisibility {
Hidden = 'HIDDEN',
Featured = 'FEATURED', // Temporarily unused feature
Visible = 'VISIBLE',
}
export interface RicherExploreEntry extends ExploreEntry {
visibility: ExploreEntryVisibility;
onInstantClick?(): void;
}
export enum ExploreFilterType {
All = 'ALL',
Keyword = 'Keyword',
}
export interface ExploreFilterMetadata {
label: string;
filterType: ExploreFilterType;
name: string;
keyword?: string;
active?: boolean;
}
export interface FAQQuestion {
prompt: string;
answer: React.ReactNode;
@@ -387,6 +426,7 @@ export enum WebsitePaths {
Credits = '/credits',
Vote = '/vote',
Extensions = '/extensions',
Explore = '/explore',
}
export enum DocPackages {

View File

@@ -11,6 +11,12 @@ export const documentConstants: { [s: string]: DocumentMetadata } = {
'0x is an open protocol that enables the peer-to-peer exchange of assets on the Ethereum blockchain.',
keywords: '',
},
EXPLORE: {
title: 'Explore 0x',
description:
'0x Protocol is free, open-source infrastructure that developers and businesses utilize to build products that enable the purchasing and trading of crypto tokens.',
keywords: '',
},
WHY: {
title: '0x: Features and Benefits',
description: