Skip to content

Commit

Permalink
multiple networks select
Browse files Browse the repository at this point in the history
uiii committed Nov 8, 2023
1 parent a44ea0c commit 112beca
Showing 5 changed files with 290 additions and 133 deletions.
296 changes: 228 additions & 68 deletions src/components/NetworkSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,61 @@
/** @jsxImportSource @emotion/react */
import { useCallback, useEffect } from "react";
import { Divider, ListItemIcon, ListItemText, ListSubheader, MenuItem, Select, SelectProps } from "@mui/material";
import { BlurOn as AllIcon } from "@mui/icons-material";
import { css } from "@emotion/react";
import { useCallback, useState } from "react";
import { Button, ButtonProps, Checkbox, Divider, ListItemIcon, ListItemText, ListSubheader, Menu, MenuItem } from "@mui/material";
import { grey } from "@mui/material/colors";
import { BlurOn as AllIcon, ArrowDropDown } from "@mui/icons-material";
import { Theme, css } from "@emotion/react";

import { useNetworks } from "../hooks/useNetworks";
import { useNetworkGroups } from "../hooks/useNetworkGroups";
import { Network } from "../model/network";

const selectStyle = css`
&.MuiInputBase-root {
.MuiSelect-select {
display: flex;
align-items: center;
padding-left: 16px;
}
import { Link } from "./Link";

const buttonStyle = css`
display: flex;
gap: 12px;
padding-left: 16px;
padding-right: 8px;
flex: 1 0 auto;
border-radius: 8px;
border: solid 1px #c4cdd5;
border-right: none;
font-size: 1rem;
font-weight: 400;
&, &:hover {
background-color: ${grey[100]};
}
`;

.MuiListItemIcon-root {
min-width: 36px;
const headerStyle = css`
display: flex;
align-items: center;
justify-content: space-between;
img {
width: 24px;
height: 24px;
}
padding-top: 12px;
padding-bottom: 20px;
line-height: 1.2;
a {
font-weight: 400;
cursor: pointer;
}
`;

const menuItemStyle = css`
.MuiListItemIcon-root {
min-width: 0px;
margin-right: 16px;
}
.MuiListItemText-root {
margin-right: 24px;
}
`;

const iconStyle = css`
@@ -34,63 +65,192 @@ const iconStyle = css`
border-radius: 0px;
`;

type NetworkSelectProps = Omit<SelectProps, "value" | "onChange"> & {
value: string | undefined;
onChange: (value: string, isUserAction: boolean) => void;
};
const checkboxStyle = css`
margin: -6px -16px;
margin-left: auto;
padding-top: 6px;
padding-bottom: 6px;
padding-right: 16px;
font-weight: 400;
&::before {
content: " ";
position: absolute;
left: 0;
height: 65%;
width: 1px;
background-color: ${grey[400]};
}
const NetworkSelect = (props: NetworkSelectProps) => {
const { value, onChange, ...selectProps } = props;
.MuiMenuItem-root:not(:hover) &::before {
display: none;
}
`;

interface NetworkSelectProps extends Omit<ButtonProps, "value" | "onChange"> {
value: Network[];
onChange: (value: Network[], isUserAction: boolean) => void;
}

export const NetworkSelect = (props: NetworkSelectProps) => {
const { value, onChange, ...buttonProps } = props;

const networkGroups = useNetworkGroups();

useEffect(() => {
if (!value) {
onChange("*", false);
const [anchorEl, setAnchorEl] = useState<HTMLElement>();

const open = !!anchorEl;

const handleClose = useCallback(() => setAnchorEl(undefined), []);

const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}, []);

const setSelection = useCallback((networks: Network[]) => {
const newValue = [];

for (const networkGroup of networkGroups) {
for (const network of networkGroup.networks) {
if (networks.includes(network)) {
newValue.push(network);
}
}
}
}, [value, onChange]);

const handleNetworkChange = useCallback(
(e: any) => {
onChange && onChange(e.target.value, true);
},
[onChange]
);
onChange?.(newValue, true);
handleClose();
}, [onChange]);

const addSelection = useCallback((networks: Network[]) => {
const newValue = [];

for (const networkGroup of networkGroups) {
for (const network of networkGroup.networks) {
if (networks.includes(network) || value.includes(network)) {
newValue.push(network);
}
}
}

onChange?.(newValue, true);
}, [value, networkGroups, onChange]);

const removeSelection = useCallback((networks: Network[]) => {
const newValue = [];

for (const networkGroup of networkGroups) {
for (const network of networkGroup.networks) {
if (value.includes(network) && !networks.includes(network)) {
newValue.push(network);
}
}
}

onChange?.(newValue, true);
}, [value, networkGroups, onChange]);

console.log("value", value);


return (
<Select
{...selectProps}
onChange={handleNetworkChange}
value={value || ""}
css={selectStyle}
>
<MenuItem value="*">
<ListItemIcon>
<AllIcon css={{color: "#e6007a"}} />
</ListItemIcon>
<ListItemText>All networks</ListItemText>
</MenuItem>
<Divider />
{networkGroups.map((group, index) => [
index > 0 && <Divider />,
<ListSubheader key={index}>
{group.relayChainNetwork?.displayName || "Other"}
{group.relayChainNetwork && <span> and parachains</span>}
</ListSubheader>,
...group.networks.map((network) => (
<MenuItem key={network.name} value={network.name}>
<ListItemIcon>
<img
src={network.icon}
css={iconStyle}
/>
</ListItemIcon>
<ListItemText>{network.displayName}</ListItemText>
</MenuItem>
))
])}
</Select>
<>
<Button
{...buttonProps}
id="basic-button"
aria-controls={open ? "networks-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
onClick={handleClick}
css={buttonStyle}
>
{value.length === 0 && (
<>
<AllIcon css={[iconStyle, {color: "#e6007a"}]} />
<span>All networks</span>
</>
)}
{value.length === 1 && (
<>
<img src={value[0]?.icon} css={iconStyle} />
<span>{value[0]?.displayName}</span>
</>
)}
{value.length > 1 && (
<>
{value.slice(0, 3).map((network) => <img src={network.icon} css={iconStyle} />)}
{value.length > 3 && <span>+ {value.length - 3} other</span>}
</>
)}
<ArrowDropDown />
</Button>
<Menu
id="networks-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
>
<MenuItem
css={menuItemStyle}
selected={value.length === 0}
onClick={() => setSelection([])}
>
<ListItemIcon>
<AllIcon css={[iconStyle, {color: "#e6007a"}]} />
</ListItemIcon>
<ListItemText>All networks</ListItemText>
</MenuItem>
{networkGroups.map((group, index) => {
const allSelected = group.networks.every(it => value.includes(it));

return (
<>
<Divider />
<ListSubheader css={headerStyle} key={index}>
<div>
{group.relayChainNetwork?.displayName || "Other"}{" "}
{group.relayChainNetwork && <><br /><span style={{fontSize: 12}}>and parachains</span></>}
</div>
<Link
onClick={() => allSelected
? removeSelection(group.networks)
: addSelection(group.networks)
}
>
{allSelected ? "deselect" : "select"} all
</Link>
</ListSubheader>
{group.networks.map((network) => (
<MenuItem
css={menuItemStyle}
selected={value.includes(network)}
onClick={() => setSelection([network])}
>
<ListItemIcon>
<img
src={network.icon}
css={iconStyle}
/>
</ListItemIcon>
<ListItemText>{network.displayName}</ListItemText>
<Checkbox
css={checkboxStyle}
checked={value.includes(network)}
onChange={(ev, checked) => checked ? addSelection([network]) : removeSelection([network])}
onClick={(ev) => ev.stopPropagation()}
disableRipple
/>
</MenuItem>
))}
</>
);
})}
</Menu>
</>
);
};

export default NetworkSelect;
87 changes: 41 additions & 46 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,10 @@ import { Button, FormGroup, TextField } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { css, Theme } from "@emotion/react";

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

import { NetworkSelect } from "./NetworkSelect";

const formGroupStyle = css`
flex-direction: row;
@@ -93,86 +96,77 @@ const buttonStyle = (theme: Theme) => css`
`;

export type SearchInputProps = FormHTMLAttributes<HTMLFormElement> & {
defaultNetwork?: string;
persistNetwork?: boolean;
onNetworkChange?: (network?: string) => void;
persist?: boolean;
};

function SearchInput(props: SearchInputProps) {
const { defaultNetwork, persistNetwork, onNetworkChange, ...restProps } = props;

console.log("default network", defaultNetwork);
const { persist, ...restProps } = props;

const [qs] = useSearchParams();
const query = qs.get("query");
console.log(qs, query);

const [network, setNetwork] = useState<string | undefined>(defaultNetwork);
const [search, setSearch] = useState<string>(query || "");
const [networks, setNetworks] = useState<Network[]>(getNetworks(qs.getAll("network") || []));
const [query, setQuery] = useState<string>(qs.get("query") || "");

const navigate = useNavigate();

const handleNetworkSelect = useCallback((network: string, isUserAction: boolean) => {
if (isUserAction && persistNetwork) {
console.log("store", network);
localStorage.setItem("network", network);
const storeNetworks = (networks: Network[]) => localStorage.setItem("networks", JSON.stringify(networks.map(it => it.name)));
const loadNetworks = () => getNetworks(JSON.parse(localStorage.getItem("networks") || "[]"));

const handleNetworkSelect = useCallback((networks: Network[], isUserAction: boolean) => {
if (isUserAction && persist) {
console.log("store", networks);
storeNetworks(networks);
}

setNetwork(network);
}, [persistNetwork]);
setNetworks(networks);
}, [persist]);

const handleSubmit = useCallback(
(e: any) => {
if (!network) {
return;
}
const handleSubmit = useCallback((ev: any) => {
ev.preventDefault();

e.preventDefault();
localStorage.setItem("network", network);
if (networks.length > 0) {
storeNetworks(networks);
}

let url = `/search?query=${search}`;
const searchParams = new URLSearchParams({
query: query
});

if (network !== "*") {
url = `${url}&network=${network}`;
}
for (const network of networks) {
searchParams.append("network", network.name);
}

console.log("SEARCH", url);
console.log("SEARCH", `/search?${searchParams.toString()}`);

navigate(url);
},
[navigate, network, search]
);
navigate(`/search?${searchParams.toString()}`);
}, [navigate, networks, query]);

useEffect(() => {
setSearch(query || "");
}, [query]);
setQuery(qs.get("query") || "");
setNetworks(getNetworks(qs.getAll("network") || []));
}, [qs]);

useEffect(() => {
if (persistNetwork) {
const network = localStorage.getItem("network");
network && setNetwork(network);
if (persist) {
setNetworks(loadNetworks());
}
}, [persistNetwork]);

useEffect(() => {
onNetworkChange?.(network);
}, [onNetworkChange, network]);
}, [persist]);

return (
<form {...restProps} onSubmit={handleSubmit}>
<FormGroup row css={formGroupStyle}>
<NetworkSelect
css={networkSelectStyle}
onChange={handleNetworkSelect}
value={network}
value={networks}
/>
<TextField
css={textFieldStyle}
fullWidth
id="search"
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => setQuery(e.target.value)}
placeholder="Extrinsic hash / account address / block hash / block height / extrinsic name / event name"
value={search}
value={query}
/>
<Button
css={buttonStyle}
@@ -181,6 +175,7 @@ function SearchInput(props: SearchInputProps) {
type="submit"
variant="contained"
color="primary"
data-class="search-button"
>
<span className="text">Search</span>
</Button>
10 changes: 5 additions & 5 deletions src/components/TabbedContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsxImportSource @emotion/react */
import { Children, cloneElement, PropsWithChildren, ReactElement, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { Children, cloneElement, HTMLAttributes, PropsWithChildren, ReactElement, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { CircularProgress, Tab, TabProps, Tabs } from "@mui/material";
import ErrorIcon from "@mui/icons-material/Warning";
import { Theme, css } from "@emotion/react";
@@ -65,14 +65,14 @@ export const TabPane = (props: TabPaneProps) => {
return <>{props.children}</>;
};

export type TabbedContentProps = {
export interface TabbedContentProps extends HTMLAttributes<HTMLDivElement> {
children: ReactElement<TabPaneProps>|(ReactElement<TabPaneProps>|false)[];
currentTab?: string;
onTabChange: (tab: string) => void;
}

export const TabbedContent = (props: TabbedContentProps) => {
const { children, currentTab: tab, onTabChange } = props;
const { children, currentTab: tab, onTabChange, ...divProps } = props;

const tabHandles = useMemo(() => Children.map(children, (child) => {
if (!child) {
@@ -128,7 +128,7 @@ export const TabbedContent = (props: TabbedContentProps) => {
}, [currentTabPane, tabHandles, onTabChange]);

return (
<>
<div {...divProps}>
<div css={tabsWrapperStyle}>
<Tabs
css={tabsStyle}
@@ -142,6 +142,6 @@ export const TabbedContent = (props: TabbedContentProps) => {

</div>
{currentTabPane}
</>
</div>
);
};
5 changes: 2 additions & 3 deletions src/screens/home.tsx
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@ const searchInputStyle = (theme: Theme) => css`
}
${theme.breakpoints.up("md")} {
.MuiButton-root {
[data-class="search-button"] {
padding-left: 52px;
padding-right: 52px;
}
@@ -210,8 +210,7 @@ export const HomePage = () => {
<div css={searchBoxStyle}>
<SearchInput
css={searchInputStyle}
defaultNetwork={"polkadot"}
persistNetwork
persist
/>
</div>
<div css={networksStyle}>
25 changes: 14 additions & 11 deletions src/screens/search.tsx
Original file line number Diff line number Diff line change
@@ -36,7 +36,8 @@ const queryStyle = css`
}
`;

const xxStyle = css`
const searchDetailsStyle = css`
margin-top: -16px;
line-height: 38px;
`;

@@ -86,6 +87,10 @@ const loadingStyle = css`
word-break: break-all;
`;

const resultsStyle = css`
margin-top: 32px;
`;

export const SearchPage = () => {
const [qs] = useSearchParams();

@@ -153,17 +158,17 @@ export const SearchPage = () => {
if (!forceLoading && searchResult.totalCount === 1) {
const extrinsicItem = searchResult.extrinsics.data?.[0];
if (extrinsicItem?.data) {
return <Navigate to={`/${extrinsicItem.network}/extrinsic/${extrinsicItem.data.id}`} replace />;
return <Navigate to={`/${extrinsicItem.network.name}/extrinsic/${extrinsicItem.data.id}`} replace />;
}

const blockItem = searchResult.blocks.data?.[0];
if (blockItem?.data) {
return <Navigate to={`/${blockItem.network}/block/${blockItem.data.id}`} replace />;
return <Navigate to={`/${blockItem.network.name}/block/${blockItem.data.id}`} replace />;
}

const accountItem = searchResult.accounts.data?.[0];
if (accountItem?.data) {
return <Navigate to={`/${accountItem.network}/account/${accountItem.data.id}`} replace />;
return <Navigate to={`/${accountItem.network.name}/account/${accountItem.data.id}`} replace />;
}
}

@@ -204,7 +209,7 @@ export const SearchPage = () => {
<CardHeader>
Search results
</CardHeader>
<div css={xxStyle}>
<div css={searchDetailsStyle}>
For query <code css={queryStyle2}>{query}</code> in {" "}
{networkNames.length === 0 && <span>all networks.</span>}
{networkNames.length > 0 && (
@@ -229,10 +234,8 @@ export const SearchPage = () => {
report
/>
)}
</Card>
{searchResult.data &&
<Card>
<TabbedContent currentTab={tab} onTabChange={setTab}>
{searchResult.data &&
<TabbedContent css={resultsStyle} currentTab={tab} onTabChange={setTab}>
<TabPane
value="accounts"
label="Accounts"
@@ -290,8 +293,8 @@ export const SearchPage = () => {
/>
</TabPane>
</TabbedContent>
</Card>
}
}
</Card>
</>
);
};

0 comments on commit 112beca

Please sign in to comment.