Skip to content

Commit 76954c8

Browse files
authored
Merge pull request #181 from topmonks/168-search-autocomplete
Search autocomplete
2 parents a09199b + f5aba83 commit 76954c8

14 files changed

+635
-263
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { LinearProgress } from "@mui/material";
3+
import { css, Theme } from "@emotion/react";
4+
5+
import { ReactComponent as Logo } from "../assets/calamar-logo-export-05.svg";
6+
import Background from "../assets/main-screen-bgr.svg";
7+
8+
import { Footer } from "./Footer";
9+
10+
import { usePreloadRuntimeMetadata } from "../hooks/usePreloadRuntimeMetadata";
11+
import { Outlet } from "react-router-dom";
12+
13+
const containerStyle = (theme: Theme) => css`
14+
--content-min-height: 900px;
15+
16+
width: 100%;
17+
margin: 0;
18+
display: flex;
19+
flex-direction: column;
20+
align-items: stretch;
21+
22+
${theme.breakpoints.up("sm")} {
23+
--content-min-height: 1000px;
24+
}
25+
26+
${theme.breakpoints.up("md")} {
27+
--content-min-height: 1100px;
28+
}
29+
30+
${theme.breakpoints.up("lg")} {
31+
--content-min-height: 1200px;
32+
}
33+
34+
${theme.breakpoints.up("xl")} {
35+
--content-min-height: 1300px;
36+
}
37+
`;
38+
39+
const contentStyle = css`
40+
position: relative;
41+
flex: 1 1 auto;
42+
min-height: var(--content-min-height);
43+
`;
44+
45+
const backgroundStyle = css`
46+
position: absolute;
47+
top: 0;
48+
margin: 0;
49+
width: 100%;
50+
height: 100%;
51+
min-height: 100vh;
52+
z-index: -1;
53+
54+
&::before {
55+
content: '';
56+
position: absolute;
57+
top: 0;
58+
left: 0;
59+
width: 100%;
60+
height: var(--content-min-height);
61+
background-color: white;
62+
background-position: center bottom;
63+
background-size: 100% auto;
64+
background-repeat: no-repeat;
65+
background-image: url(${Background});
66+
}
67+
68+
&::after {
69+
content: '';
70+
position: absolute;
71+
top: var(--content-min-height);
72+
left: 0;
73+
right: 0;
74+
bottom: 0;
75+
background-color: #9af0f7;
76+
}
77+
`;
78+
79+
const logoStyle = css`
80+
width: 420px;
81+
margin: 40px auto;
82+
display: block;
83+
max-width: 100%;
84+
`;
85+
86+
const subtitleStyle = (theme: Theme) => css`
87+
position: relative;
88+
top: -100px;
89+
padding: 0 16px;
90+
font-size: 16px;
91+
text-align: center;
92+
93+
${theme.breakpoints.down("sm")} {
94+
top: -70px;
95+
}
96+
`;
97+
98+
const footerStyle = css`
99+
flex: 0 0 auto;
100+
101+
> div {
102+
max-width: 1000px;
103+
}
104+
`;
105+
106+
const metadatLoadingStyle = css`
107+
max-width: 500px;
108+
margin: 0 auto;
109+
padding: 0 16px;
110+
111+
text-align: center;
112+
`;
113+
114+
const metadataProgressStyle = css`
115+
margin-bottom: 16px;
116+
height: 8px;
117+
118+
border-radius: 4px;
119+
background-color: #e1fbfd;
120+
121+
.MuiLinearProgress-bar {
122+
background-color: #7acbdd;
123+
}
124+
`;
125+
126+
export const RuntimeMetadataLoader = () => {
127+
const metadataPreload = usePreloadRuntimeMetadata();
128+
129+
if (metadataPreload.loading) {
130+
return (
131+
<div css={containerStyle}>
132+
<div css={backgroundStyle} data-test="background" />
133+
<div css={contentStyle}>
134+
<Logo css={logoStyle} />
135+
<div css={subtitleStyle}>Block explorer for Polkadot & Kusama ecosystem</div>
136+
<div css={metadatLoadingStyle}>
137+
<LinearProgress
138+
css={metadataProgressStyle}
139+
variant="determinate"
140+
value={metadataPreload.progress}
141+
/>
142+
<span>Loading latest runtime metadata ...</span>
143+
</div>
144+
</div>
145+
<Footer css={footerStyle} />
146+
</div>
147+
);
148+
}
149+
150+
return <Outlet />;
151+
};

src/components/SearchInput.tsx

Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
/** @jsxImportSource @emotion/react */
2-
import { FormHTMLAttributes, useCallback, useEffect, useState } from "react";
2+
import { FormHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { useNavigate, useSearchParams } from "react-router-dom";
4-
import { Button, FormGroup, TextField } from "@mui/material";
4+
import { Autocomplete, Button, FormGroup, TextField, debounce } from "@mui/material";
55
import SearchIcon from "@mui/icons-material/Search";
66
import { css, Theme } from "@emotion/react";
77

8+
import { useAutocompleteSearchQuery } from "../hooks/useAutocompleteSearchQuery";
89
import { Network } from "../model/network";
910
import { getNetworks } from "../services/networksService";
1011

1112
import { NetworkSelect } from "./NetworkSelect";
1213

14+
const formStyle = css`
15+
position: relative;
16+
text-align: left;
17+
`;
18+
1319
const formGroupStyle = css`
1420
flex-direction: row;
1521
justify-content: center;
1622
flex-wrap: nowrap;
1723
`;
1824

1925
const networkSelectStyle = (theme: Theme) => css`
20-
flex: 1 0 auto;
26+
flex: 0 0 auto;
2127
2228
border-top-right-radius: 0;
2329
border-bottom-right-radius: 0;
@@ -47,12 +53,24 @@ const networkSelectStyle = (theme: Theme) => css`
4753
min-width: 0;
4854
}
4955
50-
.MuiListItemText-root {
56+
> span {
5157
display: none;
5258
}
5359
}
5460
`;
5561

62+
const inputStyle = css`
63+
flex: 1 0 auto;
64+
65+
.MuiOutlinedInput-root {
66+
padding: 0 !important;
67+
68+
.MuiAutocomplete-input {
69+
padding: 12px 16px;
70+
}
71+
}
72+
`;
73+
5674
const textFieldStyle = css`
5775
.MuiInputBase-root {
5876
border-radius: 0;
@@ -70,6 +88,24 @@ const textFieldStyle = css`
7088
}
7189
`;
7290

91+
const autocompleteNameStyle = css`
92+
flex: 1 1 auto;
93+
overflow: hidden;
94+
text-overflow: ellipsis;
95+
padding-right: 16px;
96+
`;
97+
98+
const autocompleteTypeStyle = css`
99+
margin-left: auto;
100+
flex: 0 0 auto;
101+
font-size: 12px;
102+
opacity: .75;
103+
border: solid 1px gray;
104+
border-radius: 8px;
105+
padding: 0 4px;
106+
background-color: rgba(0, 0, 0, .025);
107+
`;
108+
73109
const buttonStyle = (theme: Theme) => css`
74110
border-radius: 8px;
75111
border-top-left-radius: 0px;
@@ -99,6 +135,14 @@ const buttonStyle = (theme: Theme) => css`
99135
}
100136
`;
101137

138+
function storeNetworks(networks: Network[]) {
139+
localStorage.setItem("networks", JSON.stringify(networks.map(it => it.name)));
140+
}
141+
142+
function loadNetworks() {
143+
return getNetworks(JSON.parse(localStorage.getItem("networks") || "[]"));
144+
}
145+
102146
export type SearchInputProps = FormHTMLAttributes<HTMLFormElement> & {
103147
persist?: boolean;
104148
defaultNetworks?: Network[];
@@ -109,13 +153,17 @@ function SearchInput(props: SearchInputProps) {
109153

110154
const [qs] = useSearchParams();
111155

156+
const navigate = useNavigate();
157+
158+
const formRef = useRef<HTMLFormElement>(null);
159+
112160
const [networks, setNetworks] = useState<Network[]>(defaultNetworks || getNetworks(qs.getAll("network") || []));
113161
const [query, setQuery] = useState<string>(qs.get("query") || "");
162+
const [autocompleteQuery, _setAutocompleteQuery] = useState<string>(query || "");
114163

115-
const navigate = useNavigate();
164+
const setAutocompleteQuery = useMemo(() => debounce(_setAutocompleteQuery, 250), []);
116165

117-
const storeNetworks = (networks: Network[]) => localStorage.setItem("networks", JSON.stringify(networks.map(it => it.name)));
118-
const loadNetworks = () => getNetworks(JSON.parse(localStorage.getItem("networks") || "[]"));
166+
const autocompleteSuggestions = useAutocompleteSearchQuery(autocompleteQuery, networks);
119167

120168
const handleNetworkSelect = useCallback((networks: Network[], isUserAction: boolean) => {
121169
if (isUserAction && persist) {
@@ -126,6 +174,11 @@ function SearchInput(props: SearchInputProps) {
126174
setNetworks(networks);
127175
}, [persist]);
128176

177+
const handleQueryChange = useCallback((ev: any, value: string) => {
178+
setQuery(value);
179+
setAutocompleteQuery(value);
180+
}, []);
181+
129182
const handleSubmit = useCallback((ev: any) => {
130183
ev.preventDefault();
131184

@@ -158,34 +211,67 @@ function SearchInput(props: SearchInputProps) {
158211
}, [persist]);
159212

160213
return (
161-
<form {...restProps} onSubmit={handleSubmit}>
162-
<FormGroup row css={formGroupStyle}>
163-
<NetworkSelect
164-
css={networkSelectStyle}
165-
onChange={handleNetworkSelect}
166-
value={networks}
167-
multiselect
168-
/>
169-
<TextField
170-
css={textFieldStyle}
171-
fullWidth
172-
id="search"
173-
onChange={(e) => setQuery(e.target.value)}
174-
placeholder="Extrinsic hash / account address / block hash / block height / extrinsic name / event name"
175-
value={query}
176-
/>
177-
<Button
178-
css={buttonStyle}
179-
onClick={handleSubmit}
180-
startIcon={<SearchIcon />}
181-
type="submit"
182-
variant="contained"
183-
color="primary"
184-
data-class="search-button"
185-
>
186-
<span className="text">Search</span>
187-
</Button>
188-
</FormGroup>
214+
<form {...restProps} css={formStyle} onSubmit={handleSubmit} data-test="search-input" ref={formRef}>
215+
<Autocomplete
216+
css={inputStyle}
217+
freeSolo
218+
includeInputInList
219+
autoComplete
220+
disableClearable
221+
options={autocompleteSuggestions.data || []}
222+
disablePortal
223+
fullWidth
224+
filterOptions={it => it}
225+
inputValue={query}
226+
onInputChange={handleQueryChange}
227+
renderOption={(props, option) => (
228+
<li {...props}>
229+
<div css={autocompleteNameStyle}>
230+
{option.label.slice(0, option.highlight[0])}
231+
<strong>{option.label.slice(option.highlight[0], option.highlight[1])}</strong>
232+
{option.label.slice(option.highlight[1])}
233+
</div>
234+
<div css={autocompleteTypeStyle}>{option.type}</div>
235+
</li>
236+
)}
237+
componentsProps={{
238+
popper: {
239+
anchorEl: formRef.current,
240+
placement: "bottom-start",
241+
style: {
242+
width: "100%"
243+
}
244+
}
245+
}}
246+
renderInput={(params) =>
247+
<FormGroup row css={formGroupStyle}>
248+
<NetworkSelect
249+
css={networkSelectStyle}
250+
onChange={handleNetworkSelect}
251+
value={networks}
252+
multiselect
253+
/>
254+
<TextField
255+
{...params}
256+
css={textFieldStyle}
257+
fullWidth
258+
id="search"
259+
placeholder="Extrinsic hash / account address / block hash / block height / extrinsic name / event name"
260+
/>
261+
<Button
262+
css={buttonStyle}
263+
onClick={handleSubmit}
264+
startIcon={<SearchIcon />}
265+
type="submit"
266+
variant="contained"
267+
color="primary"
268+
data-class="search-button"
269+
>
270+
<span className="text">Search</span>
271+
</Button>
272+
</FormGroup>
273+
}
274+
/>
189275
</form>
190276
);
191277
}

src/components/network/NetworkStats.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const statsLayoutStyle = css`
4545
display: grid;
4646
width: 100%;
4747
height: auto;
48+
margin-bottom: 32px;
4849
4950
gap: 10px;
5051

0 commit comments

Comments
 (0)