diff --git a/packages/frontend/src/components/Chain/Chain.tsx b/packages/frontend/src/components/Chain/Chain.tsx index 27c8fcc..1f1a7cb 100644 --- a/packages/frontend/src/components/Chain/Chain.tsx +++ b/packages/frontend/src/components/Chain/Chain.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { Types, Maybe } from '@dotstats/common'; -import { State as AppState, Node as NodeState } from '../../state'; +import { Types } from '@dotstats/common'; +import { State as AppState } from '../../state'; import { formatNumber, secondsWithPrecision, getHashData } from '../../utils'; -import { Tab, Filter } from './'; +import { Tab } from './'; import { Tile, Ago, List, Map, Settings } from '../'; import { PersistentObject, PersistentSet } from '../../persist'; @@ -13,8 +13,6 @@ import listIcon from '../../icons/list-alt-regular.svg'; import worldIcon from '../../icons/location.svg'; import settingsIcon from '../../icons/settings.svg'; -const ESCAPE_KEY = 27; - import './Chain.css'; export namespace Chain { @@ -28,7 +26,6 @@ export namespace Chain { export interface State { display: Display; - filter: Maybe; } } @@ -49,18 +46,9 @@ export class Chain extends React.Component { this.state = { display, - filter: null, }; } - public componentWillMount() { - window.addEventListener('keyup', this.onKeyUp); - } - - public componentWillUnmount() { - window.removeEventListener('keyup', this.onKeyUp); - } - public render() { const { appState } = this.props; const { best, blockTimestamp, blockAverage } = appState; @@ -88,7 +76,7 @@ export class Chain extends React.Component { } private renderContent() { - const { display, filter } = this.state; + const { display } = this.state; if (display === 'settings') { return ; @@ -97,57 +85,13 @@ export class Chain extends React.Component { const { appState, pins } = this.props; return ( - - - { - display === 'list' - ? - : - } - + display === 'list' + ? + : ); } private setDisplay = (display: Chain.Display) => { this.setState({ display }); }; - - private onKeyUp = (event: KeyboardEvent) => { - if (event.ctrlKey) { - return; - } - - const { filter } = this.state; - const key = event.key; - - const escape = filter != null && event.keyCode === ESCAPE_KEY; - const singleChar = filter == null && key.length === 1; - - if (escape) { - this.setState({ filter: null }); - } else if (singleChar) { - this.setState({ filter: key }); - } - } - - private onFilterChange = (filter: string) => { - this.setState({ filter }); - } - - private getNodeFilter(): Maybe<(node: NodeState) => boolean> { - const { filter } = this.state; - - if (filter == null) { - return null; - } - - const filterLC = filter.toLowerCase(); - - return ({ name, city }) => { - const matchesName = name.toLowerCase().indexOf(filterLC) !== -1; - const matchesCity = city != null && city.toLowerCase().indexOf(filterLC) !== -1; - - return matchesName || matchesCity; - } - } } diff --git a/packages/frontend/src/components/Chain/Filter.tsx b/packages/frontend/src/components/Chain/Filter.tsx deleted file mode 100644 index 8243199..0000000 --- a/packages/frontend/src/components/Chain/Filter.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; -import { Maybe } from '@dotstats/common'; -import { Icon } from '../'; - -import searchIcon from '../../icons/search.svg'; - -import './Filter.css'; - -export namespace Filter { - export interface Props { - value: Maybe; - onChange: (value: Maybe) => void; - } -} - -const ESCAPE_KEY = 27; - -export class Filter extends React.Component { - private filterInput: HTMLInputElement; - - public componentDidMount() { - this.filterInput.focus(); - } - - public shouldComponentUpdate(nextProps: Filter.Props): boolean { - if (this.props.value === nextProps.value && this.props.onChange === nextProps.onChange) { - return false; - } - - if (this.props.value == null) { - this.filterInput.focus(); - } - - return true; - } - - public render() { - const { value } = this.props; - - let className = "Filter"; - - if (value == null) { - className += " Filter-hidden"; - } - - return ( -
- - -
- ); - } - - private onRef = (el: HTMLInputElement) => { - this.filterInput = el; - } - - private onChange = () => { - const { value } = this.filterInput; - - this.props.onChange(value === '' ? null : value); - } - - private onKeyUp = (event: React.KeyboardEvent) => { - event.stopPropagation(); - - if (event.keyCode === ESCAPE_KEY) { - this.props.onChange(null); - } - } - - private onBlur = (event: React.FocusEvent) => { - if (this.props.value == null) { - this.filterInput.focus(); - } - } -} diff --git a/packages/frontend/src/components/Chain/index.ts b/packages/frontend/src/components/Chain/index.ts index a06b623..03ecb91 100644 --- a/packages/frontend/src/components/Chain/index.ts +++ b/packages/frontend/src/components/Chain/index.ts @@ -1,3 +1,2 @@ export * from './Chain'; export * from './Tab'; -export * from './Filter'; diff --git a/packages/frontend/src/components/Chain/Filter.css b/packages/frontend/src/components/Filter.css similarity index 100% rename from packages/frontend/src/components/Chain/Filter.css rename to packages/frontend/src/components/Filter.css diff --git a/packages/frontend/src/components/Filter.tsx b/packages/frontend/src/components/Filter.tsx new file mode 100644 index 0000000..748fbe4 --- /dev/null +++ b/packages/frontend/src/components/Filter.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { Maybe } from '@dotstats/common'; +import { Node } from '../state'; +import { Icon } from './'; + +import searchIcon from '../icons/search.svg'; + +import './Filter.css'; + +export namespace Filter { + export interface Props { + onChange: (value: Maybe<(node: Node) => boolean>) => void; + } + + export interface State { + value: string; + } +} + +const ESCAPE_KEY = 27; + +export class Filter extends React.Component { + public state = { + value: '' + }; + + private filterInput: HTMLInputElement; + + public componentWillMount() { + window.addEventListener('keyup', this.onWindowKeyUp); + } + + public componentWillUnmount() { + window.removeEventListener('keyup', this.onWindowKeyUp); + } + + public shouldComponentUpdate(nextProps: Filter.Props, nextState: Filter.State): boolean { + return this.props.onChange !== nextProps.onChange || this.state.value !== nextState.value; + } + + public render() { + const { value } = this.state; + + let className = "Filter"; + + if (value === '') { + className += " Filter-hidden"; + } + + return ( +
+ + +
+ ); + } + + private setValue(value: string) { + this.setState({ value }); + + this.props.onChange(this.getNodeFilter(value)); + } + + private onRef = (el: HTMLInputElement) => { + this.filterInput = el; + } + + private onChange = () => { + this.setValue(this.filterInput.value); + } + + private onKeyUp = (event: React.KeyboardEvent) => { + event.stopPropagation(); + + if (event.keyCode === ESCAPE_KEY) { + this.setValue(''); + } + } + + private onWindowKeyUp = (event: KeyboardEvent) => { + if (event.ctrlKey) { + return; + } + + const { value } = this.state; + const key = event.key; + + const escape = value && event.keyCode === ESCAPE_KEY; + const singleChar = value === '' && key.length === 1; + + if (escape) { + this.setValue(''); + } else if (singleChar) { + this.setValue(key); + this.filterInput.focus(); + } + } + + private getNodeFilter(value: string): Maybe<(node: Node) => boolean> { + if (value === '') { + return null; + } + + const filter = value.toLowerCase(); + + return ({ name, city }) => { + const matchesName = name.toLowerCase().indexOf(filter) !== -1; + const matchesCity = city != null && city.toLowerCase().indexOf(filter) !== -1; + + return matchesName || matchesCity; + } + } +} diff --git a/packages/frontend/src/components/List/List.tsx b/packages/frontend/src/components/List/List.tsx index 2a1f352..8181f5a 100644 --- a/packages/frontend/src/components/List/List.tsx +++ b/packages/frontend/src/components/List/List.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Types, Maybe } from '@dotstats/common'; +import { Filter } from '../'; import { State as AppState, Node } from '../../state'; import { Row } from './'; import { PersistentSet } from '../../persist'; @@ -14,12 +15,12 @@ import './List.css'; export namespace List { export interface Props { - filter: Maybe<(node: Node) => boolean>; appState: Readonly; pins: PersistentSet; } export interface State { + filter: Maybe<(node: Node) => boolean>; viewportHeight: number; listStart: number; listEnd: number; @@ -28,6 +29,7 @@ export namespace List { export class List extends React.Component { public state = { + filter: null, viewportHeight: viewport().height, listStart: 0, listEnd: 0, @@ -50,7 +52,8 @@ export class List extends React.Component { public render() { const { settings } = this.props.appState; - const { pins, filter } = this.props; + const { pins } = this.props; + const { filter } = this.state; const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]); let nodes = this.props.appState.nodes.sorted(); @@ -60,7 +63,10 @@ export class List extends React.Component { if (nodes.length === 0) { return ( -
¯\_(ツ)_/¯
Nothing matches
+ +
¯\_(ツ)_/¯
Nothing matches
+ +
); } } @@ -73,16 +79,19 @@ export class List extends React.Component { nodes = nodes.slice(listStart, listEnd); return ( -
- - - - { - nodes.map((node) => ) - } - -
-
+ +
+ + + + { + nodes.map((node) => ) + } + +
+
+ +
); } @@ -123,6 +132,10 @@ export class List extends React.Component { this.setState({ viewportHeight }); } + + private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => { + this.setState({ filter }); + } } function divisibleBy(n: number, dividor: number): number { diff --git a/packages/frontend/src/components/Map/Map.tsx b/packages/frontend/src/components/Map/Map.tsx index 7838cd1..a53792f 100644 --- a/packages/frontend/src/components/Map/Map.tsx +++ b/packages/frontend/src/components/Map/Map.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Types, Maybe } from '@dotstats/common'; +import { Filter } from '../'; import { State as AppState, Node } from '../../state'; import { Location } from './'; import { viewport } from '../../utils'; @@ -12,11 +13,11 @@ import './Map.css'; export namespace Map { export interface Props { - filter: Maybe<(node: Node) => boolean>; appState: Readonly; } export interface State { + filter: Maybe<(node: Node) => boolean>; width: number; height: number; top: number; @@ -25,7 +26,8 @@ export namespace Map { } export class Map extends React.Component { - public state = { + public state: Map.State = { + filter: null, width: 0, height: 0, top: 0, @@ -43,29 +45,34 @@ export class Map extends React.Component { } public render() { - const { filter, appState } = this.props; + const { appState } = this.props; + const { filter } = this.state; const nodes = appState.nodes.sorted(); return ( -
- { - nodes.map((node) => { - const { lat, lon } = node; - const focused = filter == null || filter(node); + +
+ { + nodes.map((node) => { + const { lat, lon } = node; - if (lat == null || lon == null) { - // Skip nodes with unknown location - return null; - } + const focused = filter == null || filter(node); - const position = this.pixelPosition(lat, lon); + if (lat == null || lon == null) { + // Skip nodes with unknown location + return null; + } - return ( - - ); - }) - } -
+ const position = this.pixelPosition(lat, lon); + + return ( + + ); + }) + } +
+ + ); } @@ -115,4 +122,8 @@ export class Map extends React.Component { this.setState({ top, left, width, height }); } + + private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => { + this.setState({ filter }); + } } diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index bdb25c5..04f7683 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -9,3 +9,4 @@ export * from './Ago'; export * from './OfflineIndicator'; export * from './Sparkline'; export * from './Tooltip'; +export * from './Filter';