diff --git a/packages/website/public/.DS_Store b/packages/website/public/.DS_Store deleted file mode 100644 index 902acb2aea..0000000000 Binary files a/packages/website/public/.DS_Store and /dev/null differ diff --git a/packages/website/public/index.html b/packages/website/public/index.html index 43aa52b122..b760443739 100644 --- a/packages/website/public/index.html +++ b/packages/website/public/index.html @@ -17,6 +17,7 @@ + diff --git a/packages/website/ts/components/modals/modal_contact.tsx b/packages/website/ts/components/modals/modal_contact.tsx index b753462701..752160b5b8 100644 --- a/packages/website/ts/components/modals/modal_contact.tsx +++ b/packages/website/ts/components/modals/modal_contact.tsx @@ -18,6 +18,7 @@ export enum ModalContactType { General = 'GENERAL', MarketMaker = 'MARKET_MAKER', Credits = 'CREDITS', + Explore = 'EXPLORE', } interface ServiceOptionMetadata { @@ -142,6 +143,8 @@ export class ModalContact extends React.Component { return this._renderMarketMakerFormContent(errors); case ModalContactType.Credits: return this._renderCreditsFormContent(errors); + case ModalContactType.Explore: + return this._renderExploreFormContent(errors); case ModalContactType.General: default: return this._renderGeneralFormContent(errors); @@ -218,6 +221,99 @@ export class ModalContact extends React.Component { ); } + private _renderExploreFormContent(errors: ErrorProps): React.ReactNode { + return ( + <> + + If you’re working on an awesome 0x project, we would love to share it on our explore page. Fill out the form + so we can connect you with the right person to help you get started. + + + + + + + + + + + + + + + + Details for 0x Explore page: + + + + + + + {[{label: 'Yes', name: 'yes'}, {label: 'No', name: 'no'}].map((metadata: ServiceOptionMetadata) => { + return ( + + ); + })} + + + + ); + } + private _renderCreditsFormContent(errors: ErrorProps): React.ReactNode { return ( <> diff --git a/packages/website/ts/components/ui/switch.tsx b/packages/website/ts/components/ui/switch.tsx new file mode 100644 index 0000000000..483ed8f6ee --- /dev/null +++ b/packages/website/ts/components/ui/switch.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import styled, { withTheme } from 'styled-components'; + +import { Heading } from 'ts/components/text'; + +const SwitchWrapper = styled.div` + display: flex; + justify-content: space-between; + padding: 0.5rem 0; +`; + +export interface SwitchProps { + label: string; +} + +export const Switch = (props: SwitchProps) => { + return + {props.label} + ; +}; diff --git a/packages/website/ts/globals.d.ts b/packages/website/ts/globals.d.ts index 05f3c7f886..5f918aaea6 100644 --- a/packages/website/ts/globals.d.ts +++ b/packages/website/ts/globals.d.ts @@ -10,6 +10,7 @@ declare module 'react-anchor-link-smooth-scroll'; declare module 'react-responsive'; declare module 'react-scrollable-anchor'; declare module 'react-headroom'; +declare module 'zeroExInstant'; declare module '*.json' { const json: any; diff --git a/packages/website/ts/pages/explore.tsx b/packages/website/ts/pages/explore.tsx index 4d4038ce2a..b54768ca32 100644 --- a/packages/website/ts/pages/explore.tsx +++ b/packages/website/ts/pages/explore.tsx @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; +import * as zeroExInstant from 'zeroExInstant'; import { Banner } from 'ts/components/banner'; import { DocumentTitle } from 'ts/components/document_title'; @@ -10,11 +11,13 @@ 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 { ExploreGrid, ExploreGridListTile, ExploreGridListTileVisibility, ExploreGridListTileWidth } from 'ts/pages/explore/explore_grid'; +import { EXPLORE_STATE_DIALOGS, ExploreGridDialogTile } from 'ts/pages/explore/explore_grid_state_tile'; 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 { ExploreEntry, ExploreEntryInstantMetadata, RicherExploreEntry } from 'ts/types'; import { documentConstants } from 'ts/utils/document_meta_constants'; +import { ExploreSettingsDropdown } from 'ts/pages/explore/explore_dropdown'; export interface ExploreProps {} @@ -26,9 +29,6 @@ const PROJECTS: { [s: string]: ExploreEntry } = { theme_color: '#151628', url: 'https://paradex.io/', keywords: ['relayer'], - instant: { - orderSource: '', - }, }, veil: { label: 'Veil', @@ -45,6 +45,9 @@ const PROJECTS: { [s: string]: ExploreEntry } = { theme_color: '#262626', url: 'https://radarrelay.com/', keywords: ['relayer'], + instant: { + orderSource: 'https://api.radarrelay.com/0x/v2/', + }, }, emoon: { label: 'Emoon', @@ -72,6 +75,19 @@ const PROJECTS: { [s: string]: ExploreEntry } = { }, }; +enum ExploreFilterType { + All = 'ALL', + Keyword = 'Keyword', +} + +interface ExploreFilterMetadata { + label: string; + filterType: ExploreFilterType; + name: string; + keyword?: string; + active?: boolean; +} + const FILTERS: ExploreFilterMetadata[] = [{ label: 'All', name: 'all', @@ -94,12 +110,18 @@ enum ExploreEntriesModifiers { } enum ExploreEntriesOrdering { - None = 'NONE', + None = 'None', + Latest = 'Latest', + Popular = 'Popular', } +const ORDERINGS = [ExploreEntriesOrdering.None, ExploreEntriesOrdering.Latest, ExploreEntriesOrdering.Popular]; + export class Explore extends React.Component { public state = { + isEntriesLoading: false, isContactModalOpen: false, + tiles: [] as ExploreGridListTile[], entries: [] as RicherExploreEntry[], entriesOrdering: ExploreEntriesOrdering.None, filters: FILTERS, @@ -124,10 +146,10 @@ export class Explore extends React.Component {
- +
{ ); } - private readonly _onOpenContactModal = (): void => { this.setState({ isContactModalOpen: true }); }; @@ -150,10 +171,30 @@ export class Explore extends React.Component { this.setState({ isContactModalOpen: false }); }; - private _launchInstantAsync = async (): Promise => { - + private _launchInstantAsync = (params: ExploreEntryInstantMetadata): void => { + zeroExInstant.render(params, 'body'); }; + private _generateTilesFromState = (): ExploreGridListTile[] => { + if (this.state.isEntriesLoading) { + return [{ + name: 'loading', + component: , + visibility: ExploreGridListTileVisibility.Visible, + width: ExploreGridListTileWidth.FullWidth, + }]; + } + if (_.isEmpty(this.state.tiles.filter(t => !!t.exploreEntry && t.visibility !== ExploreGridListTileVisibility.Hidden))) { + return [{ + name: 'empty', + component: , + visibility: ExploreGridListTileVisibility.Visible, + width: ExploreGridListTileWidth.FullWidth, + }]; + } + return this.state.tiles; + } + private _changeSearchResults = (query: string): void => { this.setState({ query: query.trim().toLowerCase() }, () => { this._setEntriesModifier(ExploreEntriesModifiers.Search); @@ -187,30 +228,32 @@ export class Explore extends React.Component { }; private _setEntriesModifier = async (modifier: ExploreEntriesModifiers): Promise => { - let newEntries: RicherExploreEntry[]; + let newTiles: ExploreGridListTile[]; 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; + newTiles = _.concat([], this.state.tiles).map(t => { + const newTile = _.assign({}, t); + newTile.visibility = ExploreGridListTileVisibility.Visible; + if (modifier === ExploreEntriesModifiers.Search && !!newTile.exploreEntry) { + newTile.visibility = (_.includes(newTile.exploreEntry.label.toLowerCase(), this.state.query) && ExploreGridListTileVisibility.Visible) || ExploreGridListTileVisibility.Hidden; } - return newEntry; + return newTile; }); } 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; + newTiles = _.concat([], this.state.tiles).map(t => { + const newTile = _.assign({}, t); + if (!!newTile.exploreEntry) { + newTile.visibility = _.intersectionWith(activeFilters, newTile.exploreEntry.keywords, (f, k) => k === f.name).length !== 0 ? ExploreGridListTileVisibility.Visible : ExploreGridListTileVisibility.Hidden; + if (modifier === ExploreEntriesModifiers.Search && newTile.visibility === ExploreGridListTileVisibility.Visible) { + newTile.visibility = (_.includes(newTile.exploreEntry.label.toLowerCase(), this.state.query) && ExploreGridListTileVisibility.Visible) || ExploreGridListTileVisibility.Hidden; + } } - return newEntry; + return newTile; }); } } - this.setState({ entries: newEntries}); + this.setState({ tiles: newTiles }); }; // For future versions, ordering can be determined by async processes @@ -222,9 +265,21 @@ export class Explore extends React.Component { // For future versions, the load entries function can be async private _loadEntriesAsync = async (): Promise => { - const rawEntries = _.values(PROJECTS).map(e => _.assign(e, { visibility: ExploreEntryVisibility.Visible})) as RicherExploreEntry[]; - const entries = await this._setEntriesOrderingAsync(rawEntries); - this.setState({ entries }); + this.setState({ isEntriesLoading: true }); + const rawEntries = _.values(PROJECTS); + const tiles = (await this._setEntriesOrderingAsync(rawEntries)).map(e => { + const richExploreEntry = _.assign({}, e) as RicherExploreEntry; + if (!!richExploreEntry.instant) { + richExploreEntry.onInstantClick = () => this._launchInstantAsync(richExploreEntry.instant); + } + return { + name: e.label.toLowerCase(), + exploreEntry: richExploreEntry, + visibility: ExploreGridListTileVisibility.Visible, + width: ExploreGridListTileWidth.OneThird, + }; + }); + this.setState({ entries: rawEntries, tiles, isEntriesLoading: false }); } } @@ -237,6 +292,7 @@ const ExploreHeroContentWrapper = styled.div` interface ExploreHeroProps { onSearch(query: string): void; } + const ExploreHero = (props: ExploreHeroProps) => { const onSearchDebounce = _.debounce(props.onSearch, 300); const onChange = (e: any) => { onSearchDebounce(e.target.value); }; @@ -272,14 +328,6 @@ interface ExploreToolBarProps { onFilterClick(filterName: string, active: boolean): void; } -const SettingsIconWrapper = styled.div` - padding-right: 0.4rem; - display: inline; - & > * { - transform: translateY(2px); - } -`; - const ExploreToolBar = (props: ExploreToolBarProps) => { return @@ -289,12 +337,7 @@ const ExploreToolBar = (props: ExploreToolBarProps) => { })} - - - - - Featured - + ; }; diff --git a/packages/website/ts/pages/explore/explore_dropdown.tsx b/packages/website/ts/pages/explore/explore_dropdown.tsx index e69de29bb2..2b542c92b7 100644 --- a/packages/website/ts/pages/explore/explore_dropdown.tsx +++ b/packages/website/ts/pages/explore/explore_dropdown.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { Icon } from 'ts/components/icon'; +import { Heading, Paragraph } from 'ts/components/text'; +import { Switch } from 'ts/components/ui/switch'; +import { Button as ExploreTagButton } from 'ts/pages/explore/explore_tag_button'; +import { colors } from 'ts/style/colors'; + +const ExploreSettingsDropdownButton = ({}) => { + return + + + + Settings + ; +}; + +const SettingsIconWrapper = styled.div` + padding-right: 0.4rem; + display: inline; + & > * { + transform: translateY(2px); + } +`; + +const SettingsWrapper = styled.div` + position: relative; + + @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; + } + } +`; + +interface DropdownWrapInterface { + width?: number; +} + +const DropdownWrap = styled.div` + width: ${props => props.width || 280}px; + margin-top: 16px; + padding: 16px 24px; + 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%; + right: 0%; + 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: 90%; + 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; + } +`; + +export interface ExploreSettingsDropdownProps { + orderings: string[]; +} + +export class ExploreSettingsDropdown extends React.Component { + constructor(props: ExploreSettingsDropdownProps) { + super(props); + } + + public render(): React.ReactNode { + return + + + + + ; + } +} + +const DropdownContentWrapper = styled.div` +`; + +const OrderingWrapper = styled.div` + padding-top: 20px; + margin-top: 20px; + margin-bottom: 20px; + position: relative; + + &:before { + content: ''; + width: 100%; + height: 1px; + background-color: ${props => props.theme.dropdownColor}; + opacity: 0.15; + position: absolute; + top: 0; + left: 0; + } +`; + +interface DropdownContentProps { + orderings: string[]; +} + +const DropdownContent = (props: DropdownContentProps) => { + return +
+ + + Editorial content reflects the views of the 0x core team. + +
+ + + Ordering + + {props.orderings.map(o => { + return {o}; + })} + +
; +}; diff --git a/packages/website/ts/pages/explore/explore_grid.tsx b/packages/website/ts/pages/explore/explore_grid.tsx index cfc568116d..abf888080a 100644 --- a/packages/website/ts/pages/explore/explore_grid.tsx +++ b/packages/website/ts/pages/explore/explore_grid.tsx @@ -3,10 +3,35 @@ 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'; +import { RicherExploreEntry} from 'ts/types'; export interface ExptoreGridProps { - entries: RicherExploreEntry[]; + tiles: ExploreGridListTile[]; +} + +export enum ExploreGridListTileVisibility { + Hidden = 'HIDDEN', + Visible = 'VISIBLE', +} + +export enum ExploreGridListTileWidth { + OneThird = 2, + FullWidth = 6, + Half = 3, + TwoThirds = 4, +} + +export interface ExploreGridListTile { + name: string; + visibility: ExploreGridListTileVisibility; + width?: ExploreGridListTileWidth; + exploreEntry?: RicherExploreEntry; + component?: React.ReactNode; +} + +interface RicherExploreGridListTile extends ExploreGridListTile { + gridStart: number; + gridEnd: number; } export class ExploreGrid extends React.Component { @@ -17,21 +42,58 @@ export class ExploreGrid extends React.Component { public render(): React.ReactNode { return ( - {this.props.entries.filter(e => e.visibility !== ExploreEntryVisibility.Hidden).map(e => { - return ; - })} + {this._prepareTiles().map(t => { + if (!!t.exploreEntry) { + return + + ; + } else { + return + {!!t.component && t.component} + ; + } + })} ); } + + private _prepareTiles = (): RicherExploreGridListTile[] => { + const visibleTiles = this.props.tiles.filter(t => t.visibility !== ExploreGridListTileVisibility.Hidden); + return this._generateGridValues(visibleTiles); + } + + private _generateGridValues = (tiles: ExploreGridListTile[]): RicherExploreGridListTile[] => { + let gridEndCounter = 1; + const richerTiles = tiles.map(t => { + if (gridEndCounter + t.width > (ExploreGridListTileWidth.FullWidth + 1)) { + gridEndCounter = 1; + } + const gridStart = gridEndCounter; + const gridEnd = gridEndCounter + t.width; + gridEndCounter = gridEnd; + const newTile = _.assign({ gridStart, gridEnd }, t); + return newTile as RicherExploreGridListTile; + }); + return richerTiles; + } } interface ExploreGridListProps { - } +interface ExploreGridTileWrapperProps { + gridStart: number; + gridEnd: number; +} + +const ExploreGridTileWrapper = styled.div` + grid-column-start: ${props => props.gridStart}; + grid-column-end: ${props => props.gridEnd}; +`; + const ExploreGridList = styled.div` display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(${ExploreGridListTileWidth.FullWidth}, 1fr); grid-column-gap: 1.5rem; grid-row-gap: 1.5rem; @media (max-width: 56rem) { diff --git a/packages/website/ts/pages/explore/explore_grid_state_tile.tsx b/packages/website/ts/pages/explore/explore_grid_state_tile.tsx new file mode 100644 index 0000000000..f79343b3c2 --- /dev/null +++ b/packages/website/ts/pages/explore/explore_grid_state_tile.tsx @@ -0,0 +1,46 @@ +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 { ExploreGridTileWrapper } from 'ts/pages/explore/explore_grid_tile'; + +export interface ExploreGridDialogTileProps { + dialogImageUrl?: string; + title?: string; + description: string; +} + +export const EXPLORE_STATE_DIALOGS: { [s: string]: ExploreGridDialogTileProps } = { + ERROR: { + title: 'Something went wrong.', + description: 'Try refreshing the page after a few moments', + }, + LOADING: { + description: 'Loading...', + }, + EMPTY: { + title: 'No projects found.', + description: 'Try deselecting a few tags or changing your search.', + }, +}; + +export const ExploreGridDialogTile = (props: ExploreGridDialogTileProps) => { + return + {!!props.dialogImageUrl && } + {!!props.title && {props.title}} + {props.description} + ; +}; + +const ExploreGridDialogTileWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin: 120px 0; +`; diff --git a/packages/website/ts/pages/explore/explore_grid_tile.tsx b/packages/website/ts/pages/explore/explore_grid_tile.tsx index 70b7e65ebe..e869949226 100644 --- a/packages/website/ts/pages/explore/explore_grid_tile.tsx +++ b/packages/website/ts/pages/explore/explore_grid_tile.tsx @@ -47,15 +47,15 @@ const ExploreGridTileLink = styled.a` display: block; `; -const ExploreGridTileWrapper = styled.div` +export 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); - } + border: 1px solid rgba(0, 0, 0, 0.15); + // transition: box-shadow 200ms ease-in-out; + // &:hover { + // box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.1); + // } `; const ExploreGridButtonWrapper = styled.div` diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 288f1ad466..2f2be4ac3b 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -256,30 +256,10 @@ export interface ExploreEntry { 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; diff --git a/packages/website/webpack.config.js b/packages/website/webpack.config.js index d9bdd91ad3..689fe0152a 100644 --- a/packages/website/webpack.config.js +++ b/packages/website/webpack.config.js @@ -18,6 +18,9 @@ const config = { chunkFilename: 'bundle-[name].js', publicPath: '/', }, + externals: { + zeroExInstant: 'zeroExInstant' + }, devtool: 'source-map', resolve: { modules: [path.join(__dirname, '/ts'), 'node_modules'],