-
+
Real-time updated:
diff --git a/src/v2/components/Search/index.jsx b/src/v2/components/Search/index.jsx
index 8cef8183..7b4365a3 100644
--- a/src/v2/components/Search/index.jsx
+++ b/src/v2/components/Search/index.jsx
@@ -1,40 +1,79 @@
// @flow
import React, {useState} from 'react';
+import {observer} from 'mobx-react-lite';
+import {compose, get, filter, lowerCase, contains} from 'lodash/fp';
import {InputBase, IconButton} from '@material-ui/core';
import {Search as SearchIcon} from '@material-ui/icons';
+import NodesStore from 'v2/stores/nodes';
+import SearchResult from './result';
import useStyles from './styles';
-type SearchProps = {
- onSubmit: (val: string) => void,
-};
-
-const Search = ({onSubmit}: SearchProps) => {
+const Search = () => {
const classes = useStyles();
+ const {validators} = NodesStore;
const [value, setValue] = useState('');
+ const [isDirty, setDirty] = useState(false);
+ const [isFocus, setFocus] = useState(false);
+ const [searchResult, setSearchResult] = useState([]);
+
+ const onFocus = () => setFocus(true);
+ const onBlur = () => {
+ setTimeout(() => {
+ setFocus(false);
+ }, 250);
+ };
+
+ const handleClear = () => {
+ setDirty(false);
+ setValue('');
+ setSearchResult([]);
+ };
+
const handleSearch = ({currentTarget}: SyntheticEvent
) => {
+ if (!currentTarget.value) {
+ handleClear();
+ return;
+ }
setValue(currentTarget.value);
+ const filteredValidators = filter(v => {
+ const lowerVal = lowerCase(value);
+ return (
+ compose(contains(lowerVal), lowerCase, get('nodePubkey'))(v) ||
+ compose(contains(lowerVal), lowerCase, get('identity.keybaseUsername'))(v)
+ );
+ })(validators);
+ setDirty(true);
+ setSearchResult(filteredValidators);
};
- const handleSubmit = () => {
- onSubmit(value);
- };
+
return (
-
+
);
};
-export default Search;
+export default observer(Search);
diff --git a/src/v2/components/Search/result.jsx b/src/v2/components/Search/result.jsx
new file mode 100644
index 00000000..ca2149bb
--- /dev/null
+++ b/src/v2/components/Search/result.jsx
@@ -0,0 +1,41 @@
+// @flow
+import React, {memo} from 'react';
+import {map} from 'lodash/fp';
+import {Link} from 'react-router-dom';
+import type {Validator} from 'v2/@types/validator';
+
+import useStyles from './styles';
+
+type SearchResultProps = {
+ items: Validator[],
+ onClear: () => void,
+ isDirty: boolean,
+ isFocus: boolean,
+};
+
+const SearchResult = ({isDirty, isFocus, items, onClear}: SearchResultProps) => {
+ const classes = useStyles();
+ const renderItem = ({nodePubkey}: {nodePubkey: string}) => (
+
+
+ {nodePubkey}
+
+
+ );
+ if ((!isDirty && !items.length ) || !isFocus) {
+ return null;
+ }
+
+ return (
+
+
+ {!items.length
+ ? 'There were no results for this search term.'
+ : 'Validators'}
+
+
+
+ );
+};
+
+export default memo(SearchResult);
diff --git a/src/v2/components/Search/styles.js b/src/v2/components/Search/styles.js
index dce091dc..3ccd03be 100644
--- a/src/v2/components/Search/styles.js
+++ b/src/v2/components/Search/styles.js
@@ -4,6 +4,9 @@ import getColor from 'v2/utils/getColor';
export default makeStyles(theme => {
return {
root: {
+ position: 'relative',
+ },
+ form: {
border: `1px solid ${getColor('grey')(theme)}`,
padding: 5,
display: 'flex',
@@ -20,5 +23,28 @@ export default makeStyles(theme => {
borderRadius: 0,
width: 40,
},
+ list: {
+ position: 'absolute',
+ background: getColor('white')(theme),
+ width: '100%',
+ padding: '12px 20px',
+ '& ul': {
+ padding: 0,
+ margin: 0,
+ '& a': {
+ color: getColor('grey3')(theme),
+ fontSize: 12,
+ textDecoration: 'none',
+ padding: '5px 0',
+ display: 'block',
+ '&:hover': {
+ color: getColor('dark')(theme)
+ }
+ }
+ },
+ },
+ title: {
+ color: getColor('dark')(theme)
+ }
};
});
diff --git a/src/v2/components/Validators/Detail/index.jsx b/src/v2/components/Validators/Detail/index.jsx
index 5e2c248b..70bf6a51 100644
--- a/src/v2/components/Validators/Detail/index.jsx
+++ b/src/v2/components/Validators/Detail/index.jsx
@@ -75,9 +75,20 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
const specs = [
{
- label: 'Website',
+ label: 'Address',
hint: '',
- value: identity.website || '',
+ value() {
+ return (
+
+
{nodePubkey}
+
+
+
+
+
+
+ );
+ },
},
{
label: 'Voting power',
@@ -85,9 +96,15 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
value: stake,
},
{
- label: 'Address',
+ label: 'Website',
hint: '',
- value: nodePubkey,
+ value() {
+ return identity.website ? (
+ {identity.website}
+ ) : (
+ ''
+ );
+ },
},
{
label: 'Missed blocks',
@@ -97,7 +114,15 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
{
label: 'keybase',
hint: '',
- value: identity.keybaseUsername || '',
+ value() {
+ return identity.keybaseUsername ? (
+
+ {identity.keybaseUsername}
+
+ ) : (
+ ''
+ );
+ },
},
{
label: 'commission',
@@ -134,7 +159,9 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
{label}
- {value}
+
+ {typeof value === 'function' ? value() : value}
+
);
@@ -146,11 +173,6 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
{identity.name || nodePubkey}
-
-
-
-
-
)}
@@ -159,7 +181,7 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
size="large"
fullWidth
color="primary"
- href="#"
+ href="https://github.com/solana-labs/tour-de-sol#validator-public-key-registration"
>
Connect To Keybase
diff --git a/src/v2/components/Validators/Detail/styles.js b/src/v2/components/Validators/Detail/styles.js
index 10e53ad2..dfc4c9b1 100644
--- a/src/v2/components/Validators/Detail/styles.js
+++ b/src/v2/components/Validators/Detail/styles.js
@@ -87,5 +87,17 @@ export default makeStyles(theme => ({
lineHeight: '29px',
overflow: 'hidden',
textOverflow: 'ellipsis',
+ '& a': {
+ color: getColor('main')(theme),
+ textDecoration: 'none',
+ '&:hover': {
+ textDecoration: 'underline'
+ }
+ }
},
+ address: {
+ display: 'flex',
+ alignItem: 'center',
+ color: getColor('main')(theme)
+ }
}));
diff --git a/src/v2/stores/nodes.js b/src/v2/stores/nodes.js
index 2b430f14..a06ee3ac 100644
--- a/src/v2/stores/nodes.js
+++ b/src/v2/stores/nodes.js
@@ -58,9 +58,10 @@ class Store {
get validators() {
return map(vote => {
- const {lng, lat, gossip} = find({pubkey: vote.nodePubkey})(
+ const cluster = find({pubkey: vote.nodePubkey})(
this.cluster.cluster,
- );
+ ) || {};
+ const {lng = 0, lat = 0, gossip} = cluster;
return {
...vote,
coordinates: [lng, lat],
@@ -83,6 +84,7 @@ decorate(Store, {
cluster: observable,
updateClusterInfo: action.bound,
mapMarkers: computed,
+ validators: computed,
fetchClusterInfo: action.bound,
});