Skip to content

Commit

Permalink
Merge pull request #181 from topmonks/168-search-autocomplete
Browse files Browse the repository at this point in the history
Search autocomplete
  • Loading branch information
uiii committed Dec 6, 2023
2 parents a09199b + f5aba83 commit 76954c8
Show file tree
Hide file tree
Showing 14 changed files with 635 additions and 263 deletions.
151 changes: 151 additions & 0 deletions src/components/RuntimeMetadataLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/** @jsxImportSource @emotion/react */
import { LinearProgress } from "@mui/material";
import { css, Theme } from "@emotion/react";

import { ReactComponent as Logo } from "../assets/calamar-logo-export-05.svg";
import Background from "../assets/main-screen-bgr.svg";

import { Footer } from "./Footer";

import { usePreloadRuntimeMetadata } from "../hooks/usePreloadRuntimeMetadata";
import { Outlet } from "react-router-dom";

const containerStyle = (theme: Theme) => css`
--content-min-height: 900px;
width: 100%;
margin: 0;
display: flex;
flex-direction: column;
align-items: stretch;
${theme.breakpoints.up("sm")} {
--content-min-height: 1000px;
}
${theme.breakpoints.up("md")} {
--content-min-height: 1100px;
}
${theme.breakpoints.up("lg")} {
--content-min-height: 1200px;
}
${theme.breakpoints.up("xl")} {
--content-min-height: 1300px;
}
`;

const contentStyle = css`
position: relative;
flex: 1 1 auto;
min-height: var(--content-min-height);
`;

const backgroundStyle = css`
position: absolute;
top: 0;
margin: 0;
width: 100%;
height: 100%;
min-height: 100vh;
z-index: -1;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: var(--content-min-height);
background-color: white;
background-position: center bottom;
background-size: 100% auto;
background-repeat: no-repeat;
background-image: url(${Background});
}
&::after {
content: '';
position: absolute;
top: var(--content-min-height);
left: 0;
right: 0;
bottom: 0;
background-color: #9af0f7;
}
`;

const logoStyle = css`
width: 420px;
margin: 40px auto;
display: block;
max-width: 100%;
`;

const subtitleStyle = (theme: Theme) => css`
position: relative;
top: -100px;
padding: 0 16px;
font-size: 16px;
text-align: center;
${theme.breakpoints.down("sm")} {
top: -70px;
}
`;

const footerStyle = css`
flex: 0 0 auto;
> div {
max-width: 1000px;
}
`;

const metadatLoadingStyle = css`
max-width: 500px;
margin: 0 auto;
padding: 0 16px;
text-align: center;
`;

const metadataProgressStyle = css`
margin-bottom: 16px;
height: 8px;
border-radius: 4px;
background-color: #e1fbfd;
.MuiLinearProgress-bar {
background-color: #7acbdd;
}
`;

export const RuntimeMetadataLoader = () => {
const metadataPreload = usePreloadRuntimeMetadata();

if (metadataPreload.loading) {
return (
<div css={containerStyle}>
<div css={backgroundStyle} data-test="background" />
<div css={contentStyle}>
<Logo css={logoStyle} />
<div css={subtitleStyle}>Block explorer for Polkadot & Kusama ecosystem</div>
<div css={metadatLoadingStyle}>
<LinearProgress
css={metadataProgressStyle}
variant="determinate"
value={metadataPreload.progress}
/>
<span>Loading latest runtime metadata ...</span>
</div>
</div>
<Footer css={footerStyle} />
</div>
);
}

return <Outlet />;
};
156 changes: 121 additions & 35 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
/** @jsxImportSource @emotion/react */
import { FormHTMLAttributes, useCallback, useEffect, useState } from "react";
import { FormHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button, FormGroup, TextField } from "@mui/material";
import { Autocomplete, Button, FormGroup, TextField, debounce } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { css, Theme } from "@emotion/react";

import { useAutocompleteSearchQuery } from "../hooks/useAutocompleteSearchQuery";
import { Network } from "../model/network";
import { getNetworks } from "../services/networksService";

import { NetworkSelect } from "./NetworkSelect";

const formStyle = css`
position: relative;
text-align: left;
`;

const formGroupStyle = css`
flex-direction: row;
justify-content: center;
flex-wrap: nowrap;
`;

const networkSelectStyle = (theme: Theme) => css`
flex: 1 0 auto;
flex: 0 0 auto;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
Expand Down Expand Up @@ -47,12 +53,24 @@ const networkSelectStyle = (theme: Theme) => css`
min-width: 0;
}
.MuiListItemText-root {
> span {
display: none;
}
}
`;

const inputStyle = css`
flex: 1 0 auto;
.MuiOutlinedInput-root {
padding: 0 !important;
.MuiAutocomplete-input {
padding: 12px 16px;
}
}
`;

const textFieldStyle = css`
.MuiInputBase-root {
border-radius: 0;
Expand All @@ -70,6 +88,24 @@ const textFieldStyle = css`
}
`;

const autocompleteNameStyle = css`
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 16px;
`;

const autocompleteTypeStyle = css`
margin-left: auto;
flex: 0 0 auto;
font-size: 12px;
opacity: .75;
border: solid 1px gray;
border-radius: 8px;
padding: 0 4px;
background-color: rgba(0, 0, 0, .025);
`;

const buttonStyle = (theme: Theme) => css`
border-radius: 8px;
border-top-left-radius: 0px;
Expand Down Expand Up @@ -99,6 +135,14 @@ const buttonStyle = (theme: Theme) => css`
}
`;

function storeNetworks(networks: Network[]) {
localStorage.setItem("networks", JSON.stringify(networks.map(it => it.name)));
}

function loadNetworks() {
return getNetworks(JSON.parse(localStorage.getItem("networks") || "[]"));
}

export type SearchInputProps = FormHTMLAttributes<HTMLFormElement> & {
persist?: boolean;
defaultNetworks?: Network[];
Expand All @@ -109,13 +153,17 @@ function SearchInput(props: SearchInputProps) {

const [qs] = useSearchParams();

const navigate = useNavigate();

const formRef = useRef<HTMLFormElement>(null);

const [networks, setNetworks] = useState<Network[]>(defaultNetworks || getNetworks(qs.getAll("network") || []));
const [query, setQuery] = useState<string>(qs.get("query") || "");
const [autocompleteQuery, _setAutocompleteQuery] = useState<string>(query || "");

const navigate = useNavigate();
const setAutocompleteQuery = useMemo(() => debounce(_setAutocompleteQuery, 250), []);

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

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

const handleQueryChange = useCallback((ev: any, value: string) => {
setQuery(value);
setAutocompleteQuery(value);
}, []);

const handleSubmit = useCallback((ev: any) => {
ev.preventDefault();

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

return (
<form {...restProps} onSubmit={handleSubmit}>
<FormGroup row css={formGroupStyle}>
<NetworkSelect
css={networkSelectStyle}
onChange={handleNetworkSelect}
value={networks}
multiselect
/>
<TextField
css={textFieldStyle}
fullWidth
id="search"
onChange={(e) => setQuery(e.target.value)}
placeholder="Extrinsic hash / account address / block hash / block height / extrinsic name / event name"
value={query}
/>
<Button
css={buttonStyle}
onClick={handleSubmit}
startIcon={<SearchIcon />}
type="submit"
variant="contained"
color="primary"
data-class="search-button"
>
<span className="text">Search</span>
</Button>
</FormGroup>
<form {...restProps} css={formStyle} onSubmit={handleSubmit} data-test="search-input" ref={formRef}>
<Autocomplete
css={inputStyle}
freeSolo
includeInputInList
autoComplete
disableClearable
options={autocompleteSuggestions.data || []}
disablePortal
fullWidth
filterOptions={it => it}
inputValue={query}
onInputChange={handleQueryChange}
renderOption={(props, option) => (
<li {...props}>
<div css={autocompleteNameStyle}>
{option.label.slice(0, option.highlight[0])}
<strong>{option.label.slice(option.highlight[0], option.highlight[1])}</strong>
{option.label.slice(option.highlight[1])}
</div>
<div css={autocompleteTypeStyle}>{option.type}</div>
</li>
)}
componentsProps={{
popper: {
anchorEl: formRef.current,
placement: "bottom-start",
style: {
width: "100%"
}
}
}}
renderInput={(params) =>
<FormGroup row css={formGroupStyle}>
<NetworkSelect
css={networkSelectStyle}
onChange={handleNetworkSelect}
value={networks}
multiselect
/>
<TextField
{...params}
css={textFieldStyle}
fullWidth
id="search"
placeholder="Extrinsic hash / account address / block hash / block height / extrinsic name / event name"
/>
<Button
css={buttonStyle}
onClick={handleSubmit}
startIcon={<SearchIcon />}
type="submit"
variant="contained"
color="primary"
data-class="search-button"
>
<span className="text">Search</span>
</Button>
</FormGroup>
}
/>
</form>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/components/network/NetworkStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const statsLayoutStyle = css`
display: grid;
width: 100%;
height: auto;
margin-bottom: 32px;
gap: 10px;
Expand Down
Loading

0 comments on commit 76954c8

Please sign in to comment.