2019-04-05 15:01:07 -07:00

403 lines
15 KiB
TypeScript

import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import * as zeroExInstant from 'zeroExInstant';
import { DocumentTitle } from 'ts/components/document_title';
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 { addFadeInAnimation } from 'ts/constants/animations';
import { EDITORIAL, FILTERS, ORDERINGS, PROJECTS } from 'ts/pages/explore/explore_content';
import { ExploreSettingsDropdown } from 'ts/pages/explore/explore_dropdown';
import { ExploreGrid } from 'ts/pages/explore/explore_grid';
import { EXPLORE_STATE_DIALOGS, ExploreGridDialogTile } from 'ts/pages/explore/explore_grid_state_tile';
import { ExploreTagButton } from 'ts/pages/explore/explore_tag_button';
import { analytics } from 'ts/utils/analytics';
import {
ExploreAnalyticAction,
ExploreFilterMetadata,
ExploreProject,
ExploreProjectInstantMetadata,
ExploreTile,
ExploreTilesModifiers,
ExploreTilesOrdering,
ExploreTilesOrderingMetadata,
ExploreTilesOrderingType,
ExploreTileVisibility,
ExploreTileWidth,
} from 'ts/types';
import { documentConstants } from 'ts/utils/document_meta_constants';
export interface ExploreProps {}
interface ExploreModifierOptions {
filter?: ExploreFilterMetadata;
query?: string;
isEditorialShown?: boolean;
tilesOrdering?: ExploreTilesOrdering;
}
export class Explore extends React.Component<ExploreProps> {
public state = {
isTilesLoading: false,
tiles: [] as ExploreTile[],
tilesOrdering: ExploreTilesOrdering.Popular,
isEditorialShown: true,
filters: FILTERS,
query: '',
};
private readonly _debouncedChangeSearchResults: (query: string) => void;
constructor(props: ExploreProps) {
super(props);
this._debouncedChangeSearchResults = _.debounce(this._changeSearchResults, 300);
}
// tslint:disable-next-line:async-suffix
public async componentDidMount(): Promise<void> {
await this._loadEntriesAsync();
await this._setFilter('all');
}
public render(): React.ReactNode {
return (
<SiteWrap theme="light">
<DocumentTitle {...documentConstants.EXPLORE} />
<ExploreHero query={this.state.query} onSearch={this._setNewQuery} />
<Section isPadded={false} padding={'0 0 60px 0'} maxWidth={'1150px'}>
<ExploreToolBar
onFilterClick={this._setFilter}
filters={this.state.filters}
orderings={_.values(ORDERINGS)}
activeOrdering={this.state.tilesOrdering}
onOrdering={this._onOrdering}
/>
<ExploreGrid tiles={this._generateTilesFromState()} />
</Section>
</SiteWrap>
);
}
// tslint:disable-next-line:no-unused-variable
private readonly _onEditorial = async (newValue: boolean): Promise<void> => {
const newTiles = await this._generateTilesWithModifier(this.state.tiles, ExploreTilesModifiers.Editorial, {
isEditorialShown: newValue,
});
this.setState({ isEditorialShown: newValue, tiles: newTiles });
};
private readonly _onOrdering = async (newValue: string): Promise<void> => {
this.setState({ tilesOrdering: newValue });
const newTiles = await this._generateTilesWithModifier(this.state.tiles, ExploreTilesModifiers.Ordering, {
tilesOrdering: newValue as ExploreTilesOrdering,
});
this.setState({ tilesOrdering: newValue, tiles: newTiles });
};
private readonly _launchInstantAsync = (params: ExploreProjectInstantMetadata): void => {
zeroExInstant.render(params, 'body');
};
private readonly _onAnalytics = (metadata: any, action: ExploreAnalyticAction): void => {
switch (action) {
case ExploreAnalyticAction.InstantClick:
analytics.track('Explore - Instant - Clicked', { name: metadata.name });
break;
case ExploreAnalyticAction.LinkClick:
analytics.track('Explore - Link - Clicked', { name: metadata.name });
break;
case ExploreAnalyticAction.FilterClick:
analytics.track('Explore - Filter - Clicked', { filterName: metadata.filterName });
break;
case ExploreAnalyticAction.QuerySearched:
analytics.track('Explore - Query - Searched', { query: metadata.query });
break;
default:
break;
}
};
// tslint:disable-next-line:no-unused-variable
private _generateEditorialContent(): void {
this.setState({ tiles: _.concat([], EDITORIAL, this.state.tiles) });
}
private readonly _generateTilesFromState = (): ExploreTile[] => {
if (this.state.isTilesLoading) {
return [
{
name: 'loading',
component: <ExploreGridDialogTile {...EXPLORE_STATE_DIALOGS.LOADING} />,
visibility: ExploreTileVisibility.Visible,
width: ExploreTileWidth.FullWidth,
},
];
}
if (_.isEmpty(this.state.tiles.filter(t => t.visibility !== ExploreTileVisibility.Hidden))) {
return [
{
name: 'empty',
component: <ExploreGridDialogTile {...EXPLORE_STATE_DIALOGS.EMPTY} />,
visibility: ExploreTileVisibility.Visible,
width: ExploreTileWidth.FullWidth,
},
];
}
return this.state.tiles;
};
private readonly _setNewQuery = (query: string): void => {
this.setState({ query });
this._debouncedChangeSearchResults(query);
};
private readonly _changeSearchResults = async (query: string): Promise<void> => {
this._onAnalytics({ query }, ExploreAnalyticAction.QuerySearched);
const searchedTiles = await this._generateTilesWithModifier(this.state.tiles, ExploreTilesModifiers.Search, {
query,
filter: this.state.filters.find(f => f.active),
});
this.setState({ tiles: searchedTiles });
};
private readonly _setFilter = async (filterName: string, active: boolean = true): Promise<void> => {
if (active) {
this._onAnalytics({ filterName }, ExploreAnalyticAction.FilterClick);
}
let updatedFilters: ExploreFilterMetadata[];
updatedFilters = this.state.filters.map(f => {
const newFilter = _.assign({}, f);
newFilter.active = newFilter.name === filterName ? active : false;
return newFilter;
});
// If no filters are enabled, default to all
if (_.filter(updatedFilters, f => f.active).length === 0) {
await this._setFilter('all');
} else {
const newTiles = await this._generateTilesWithModifier(
this.state.tiles,
_.isEmpty(this.state.query) ? ExploreTilesModifiers.Filter : ExploreTilesModifiers.Search,
{
filter: updatedFilters.find(f => f.active),
query: this.state.query,
},
);
this.setState({ filters: updatedFilters, tiles: newTiles });
}
};
private readonly _verifyExploreTilesModifierOptions = (
modifier: ExploreTilesModifiers,
options: ExploreModifierOptions,
): boolean => {
if (modifier === ExploreTilesModifiers.Ordering) {
return _.has(options, 'tilesOrdering');
}
if (modifier === ExploreTilesModifiers.Editorial) {
return _.has(options, 'isEditorialShown');
}
if (modifier === ExploreTilesModifiers.Search) {
return _.has(options, 'filter') && _.has(options, 'query');
}
if (modifier === ExploreTilesModifiers.Filter) {
return _.has(options, 'filter');
}
return false;
};
private readonly _generateTilesWithModifier = async (
tiles: ExploreTile[],
modifier: ExploreTilesModifiers,
options: ExploreModifierOptions,
): Promise<ExploreTile[]> => {
const trimmedQuery = modifier === ExploreTilesModifiers.Search ? options.query.trim().toLowerCase() : '';
if (!this._verifyExploreTilesModifierOptions(modifier, options)) {
return tiles;
}
if (modifier === ExploreTilesModifiers.Ordering) {
switch (ORDERINGS[options.tilesOrdering].type) {
case ExploreTilesOrderingType.HardCodedByName:
return _.sortBy(tiles, t => _.indexOf(ORDERINGS[options.tilesOrdering].hardCoded, t.name));
case ExploreTilesOrderingType.DynamicBySortFunction:
return ORDERINGS[options.tilesOrdering].sort(tiles);
default:
return tiles;
}
}
return _.concat([], tiles).map(t => {
const newTile = _.assign({}, t);
if (modifier === ExploreTilesModifiers.Filter || modifier === ExploreTilesModifiers.Search) {
newTile.visibility =
(options.filter.name === 'all' && ExploreTileVisibility.Visible) || newTile.visibility;
if (!(options.filter.name === 'all') && !!newTile.exploreProject) {
newTile.visibility =
(_.includes(newTile.exploreProject.keywords, options.filter.name) &&
ExploreTileVisibility.Visible) ||
ExploreTileVisibility.Hidden;
}
}
if (
!!newTile.exploreProject &&
modifier === ExploreTilesModifiers.Search &&
newTile.visibility === ExploreTileVisibility.Visible
) {
newTile.visibility =
(_.chain(newTile.exploreProject.label.toLowerCase())
.split(' ')
.concat([newTile.exploreProject.label.toLowerCase()])
.reduce((a: boolean, s: string) => a || _.startsWith(s, trimmedQuery), false)
.value() &&
ExploreTileVisibility.Visible) ||
ExploreTileVisibility.Hidden;
}
if (modifier === ExploreTilesModifiers.Editorial) {
if (_.startsWith(t.name, 'editorial')) {
newTile.visibility = options.isEditorialShown
? ExploreTileVisibility.Visible
: ExploreTileVisibility.Hidden;
}
}
return newTile;
});
};
// For future versions, the load entries function can be async
private readonly _loadEntriesAsync = async (): Promise<void> => {
this.setState({ isEntriesLoading: true });
const rawProjects = _.values(PROJECTS);
const tiles = rawProjects.map((e: ExploreProject) => {
const exploreProject = _.assign({}, e);
if (!!exploreProject.instant) {
exploreProject.instant = _.assign({}, exploreProject.instant);
exploreProject.onInstantClick = this._launchInstantAsync.bind(this, exploreProject.instant);
}
exploreProject.onAnalytics = this._onAnalytics.bind(this, exploreProject);
return {
name: e.name,
exploreProject,
visibility: ExploreTileVisibility.Visible,
width: ExploreTileWidth.OneThird,
};
});
const orderedTiles = await this._generateTilesWithModifier(tiles, ExploreTilesModifiers.Ordering, {
tilesOrdering: this.state.tilesOrdering,
});
this.setState({ tiles: orderedTiles, isEntriesLoading: false });
};
}
const ExploreHeroContentWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 100px 0;
${addFadeInAnimation('0.5s')}
@media (max-width: 36rem) {
display: block;
padding: 50px 0;
}
`;
const ExploreSearchInputWrapper = styled.div`
width: 22rem;
@media (max-width: 52rem) {
width: 16rem;
}
@media (max-width: 36rem) {
margin-top: 10px;
padding-left: 5px;
width: 100%;
}
`;
interface ExploreHeroProps {
query: string;
onSearch(query: string): void;
}
const ExploreHero = (props: ExploreHeroProps) => {
const onChange = (e: any) => {
props.onSearch(e.target.value);
};
return (
<Section maxWidth={'1150px'} isPadded={false}>
<ExploreHeroContentWrapper>
<Heading isNoMargin={true} size="large">
Explore 0x
</Heading>
<ExploreSearchInputWrapper>
<SearchInput value={props.query} onChange={onChange} placeholder="Search..." />
</ExploreSearchInputWrapper>
</ExploreHeroContentWrapper>
</Section>
);
};
const ExploreToolBarWrapper = styled.div`
display: flex;
justify-content: space-between;
z-index: 1;
position: relative;
${addFadeInAnimation('0.5s', '0.15s')}
@media (max-width: 36rem) {
display: block;
}
`;
const ExploreToolBarContentWrapper = styled.div`
display: inline-block;
white-space: nowrap;
padding-bottom: 0.4rem
margin-bottom: 1.6rem;
overflow-x: auto;
@media (max-width: 64rem) {
& > * {
display: none;
}
}
& > * {
margin: 0 0.3rem;
}
& *:first-child {
margin-left: 0;
}
& *:last-child {
margin-right: 0;
}
`;
interface ExploreToolBarProps {
filters: ExploreFilterMetadata[];
activeOrdering: ExploreTilesOrdering;
orderings: ExploreTilesOrderingMetadata[];
editorial?: boolean;
onOrdering: (newValue: string) => void;
onEditorial?: (newValue: boolean) => void;
onFilterClick(filterName: string, active: boolean): void;
}
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>
<ExploreSettingsDropdown {...props} />
</ExploreToolBarWrapper>
);
};