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 { 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 { await this._loadEntriesAsync(); await this._setFilter('all'); } public render(): React.ReactNode { return (
); } // tslint:disable-next-line:no-unused-variable private readonly _onEditorial = async (newValue: boolean): Promise => { 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 => { 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: , visibility: ExploreTileVisibility.Visible, width: ExploreTileWidth.FullWidth, }, ]; } if (_.isEmpty(this.state.tiles.filter(t => t.visibility !== ExploreTileVisibility.Hidden))) { return [ { name: 'empty', component: , 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 => { 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 => { 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 => { 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 => { 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 (
Explore 0x
); }; 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 ( {!!props.filters && props.filters.map(f => { const onClick = () => { props.onFilterClick(f.name, !f.active); }; return ( {f.label} ); })} ); };