Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,97 +1,15 @@
"use client";

import { MultiSelect } from "@/components/blocks/multi-select";
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
import { Select } from "chakra-react-select";
import type { SizeProp } from "chakra-react-select";
import { useCallback, useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { useAllChainsData } from "../../../hooks/chains/allChains";

interface NetworkDropdownProps {
useCleanChainName?: boolean;
isDisabled?: boolean;
onSingleChange: (networksEnabled: number) => void;
value: number | undefined;
size?: SizeProp;
}

function cleanChainName(chainName: string) {
return chainName.replace("Mainnet", "");
}

export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
useCleanChainName = true,
onSingleChange,
value,
size = "md",
}) => {
const form = useFormContext();
const { allChains } = useAllChainsData();

const options = useMemo(() => {
return allChains.map((chain) => {
return {
label: useCleanChainName
? cleanChainName(chain.name)
: `${chain.name} (${chain.chainId})`,
value: chain.chainId,
};
});
}, [allChains, useCleanChainName]);

const defaultValues = useMemo(() => {
const networksEnabled = form?.watch(
"networksForDeployment.networksEnabled",
);

if (networksEnabled) {
return options.filter(({ value: val }) =>
form.watch("networksForDeployment.networksEnabled")?.includes(val),
);
}
return options;
}, [form, options]);

return (
<div className="flex w-full flex-row items-center gap-2">
<Select
size={size}
placeholder={"Select a network"}
selectedOptionStyle="check"
hideSelectedOptions={false}
options={options}
defaultValue={defaultValues}
onChange={(selectedChain) => {
if (selectedChain) {
if (onSingleChange) {
onSingleChange(selectedChain.value);
}
}
}}
chakraStyles={{
container: (provided) => ({
...provided,
width: "full",
}),
downChevron: (provided) => ({
...provided,
color: "hsl(var(--text-muted-foreground)/50%)",
}),
dropdownIndicator: (provided) => ({
...provided,
color: "hsl(var(--text-muted-foreground)/50%)",
}),
control: (provided) => ({
...provided,
borderRadius: "lg",
minWidth: "178px",
}),
}}
value={options.find(({ value: val }) => val === value)}
/>
</div>
);
};

type Option = { label: string; value: string };

export function MultiNetworkSelector(props: {
Expand Down Expand Up @@ -160,3 +78,68 @@ export function MultiNetworkSelector(props: {
/>
);
}

export function SingleNetworkSelector(props: {
chainId: number | undefined;
onChange: (chainId: number) => void;
}) {
const { allChains, idToChain } = useAllChainsData();

const options = useMemo(() => {
return allChains.map((chain) => {
return {
label: chain.name,
value: String(chain.chainId),
};
});
}, [allChains]);

const searchFn = useCallback(
(option: Option, searchValue: string) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return false;
}

if (Number.isInteger(Number.parseInt(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
},
[idToChain],
);

const renderOption = useCallback(
(option: Option) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return option.label;
}

return (
<div className="flex justify-between gap-4">
<span className="grow truncate text-left">{chain.name}</span>
<Badge variant="outline" className="gap-2">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
</div>
);
},
[idToChain],
);

return (
<SelectWithSearch
searchPlaceholder="Search by Name or Chain Id"
value={String(props.chainId)}
options={options}
onValueChange={(chainId) => {
props.onChange(Number(chainId));
}}
placeholder="Select Chain"
overrideSearchFn={searchFn}
renderOption={renderOption}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { SelectWithSearch } from "./select-with-search";

const meta = {
title: "blocks/SelectWithSearch",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Desktop: Story = {
args: {},
};

export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};

function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
label: `Item ${i}`,
}));
}

function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<VariantTest storyLabel="5 items" listLen={5} />
<VariantTest storyLabel="5000 items" listLen={5000} />
<VariantTest
defaultValue={"3"}
storyLabel="20 items, 3 selected by default"
listLen={20}
/>
</div>
);
}

function VariantTest(props: {
defaultValue?: string;
storyLabel: string;
listLen: number;
}) {
const list = useMemo(() => createList(props.listLen), [props.listLen]);
const [value, setValue] = useState<string | undefined>(props.defaultValue);

return (
<BadgeContainer label={props.storyLabel}>
<SelectWithSearch
value={value}
options={list}
onValueChange={setValue}
placeholder="Select items"
/>
</BadgeContainer>
);
}
Loading
Loading