Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize transfer chain selector ux #804

Merged
merged 1 commit into from
Aug 28, 2024
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
6 changes: 3 additions & 3 deletions apps/web/src/components/transfer-chain-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChainConfig, Token } from "../types";
import TransferSection from "./transfer-section";
import TransferChainSelect from "./transfer-chain-select";
import TransferChainSelector from "./transfer-chain-selector";
import TransferSwitch from "./transfer-switch";
import ComponentLoading from "../ui/component-loading";
import { Address } from "viem";
Expand Down Expand Up @@ -62,7 +62,7 @@ export default function TransferChainSection({
<div className="relative flex flex-col">
<ComponentLoading loading={loading} className="rounded-large backdrop-blur-[2px]" icon={false} />
<TransferSection titleText="From" titleTips={<TokenTips token={sourceToken} chain={sourceChain} />}>
<TransferChainSelect
<TransferChainSelector
chain={sourceChain}
token={sourceToken}
chainOptions={sourceChainOptions}
Expand All @@ -82,7 +82,7 @@ export default function TransferChainSection({
onExpandRecipient={onExpandRecipient}
onRecipientChange={onRecipientChange}
>
<TransferChainSelect
<TransferChainSelector
chain={targetChain}
token={targetToken}
chainOptions={targetChainOptions}
Expand Down
246 changes: 71 additions & 175 deletions apps/web/src/components/transfer-chain-select.tsx
Original file line number Diff line number Diff line change
@@ -1,186 +1,82 @@
import { useApp } from "../hooks";
import { ChainConfig, Token } from "../types";
import Select from "../ui/select";
import { formatBalance, getChainLogoSrc, getTokenLogoSrc } from "../utils";
import { useState } from "react";
import {
FloatingPortal,
offset,
size,
useClick,
useDismiss,
useFloating,
useInteractions,
useTransitionStyles,
} from "@floating-ui/react";
import { PropsWithChildren, useState } from "react";

interface Props {
chain: ChainConfig;
token: Token;
chainOptions: ChainConfig[];
tokenOptions: Token[];
onChainChange?: (chain: ChainConfig) => void;
onTokenChange?: (token: Token) => void;
label: JSX.Element;
}

export default function TransferChainSelect({
chain,
token,
chainOptions,
tokenOptions,
onChainChange,
onTokenChange,
}: Props) {
const [search, setSearch] = useState("");
export default function TransferChainSelect({ children, label }: PropsWithChildren<Props>) {
const [isOpen, setIsOpen] = useState(false);

return (
<div className="flex items-center">
<Select
placeholder={<span className="text-base font-bold text-slate-400">Select a chain</span>}
label={
<div className="gap-medium flex items-center transition-transform group-hover:translate-x-2">
<img
width={32}
height={32}
alt="Chain"
src={getChainLogoSrc(chain.logo)}
className="h-[2rem] w-[2rem] shrink-0 rounded-full"
/>
<span className="truncate text-base font-bold text-white">{chain.name}</span>
</div>
}
labelClassName="flex items-center justify-between gap-small w-full mx-medium transition-colors hover:bg-white/5 group py-small rounded-[0.625rem]"
childClassName="py-medium rounded-large bg-[#00141D] border border-white/20 flex flex-col gap-2"
offsetSize={6}
sameWidth
>
{chainOptions.length ? (
<>
<div className="mx-medium px-medium flex items-center gap-1 rounded-xl bg-white/5 transition-colors focus-within:bg-white/10 focus-within:outline-none hover:bg-white/10">
<img alt="Search" width={24} height={24} src="images/search.svg" className="h-6 w-6 opacity-60" />
<input
className="w-full bg-transparent py-2 text-base font-medium focus-visible:outline-none"
placeholder="Search ..."
value={search}
onClick={(e) => {
e.stopPropagation();
}}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
</div>
<div className="mx-auto h-[1px] w-5 bg-white/50" />
<div className="app-scrollbar flex max-h-[17.2rem] flex-col overflow-y-auto">
{chainOptions
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map((option) => (
<ChainOption
key={option.id}
selected={chain}
option={option}
token={token}
onSelect={onChainChange}
/>
))}
</div>
</>
) : (
<div className="py-medium flex justify-center">
<span className="text-sm font-bold text-slate-400">No data</span>
</div>
)}
</Select>
const { refs, context, floatingStyles } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: "bottom",
middleware: [
offset(6),
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, { width: `${rects.reference.width}px` });
},
}),
],
});

{tokenOptions.length > 1 ? (
<Select
placeholder={<span className="text-sm font-bold text-slate-400">Select a token</span>}
label={
<div className="gap-small flex items-center">
<img
width={26}
height={26}
alt="Token"
src={getTokenLogoSrc(token.logo)}
className="h-[1.625rem] w-[1.625rem] shrink-0 rounded-full"
/>
<span className="truncate text-sm font-bold text-white">{token.symbol}</span>
</div>
}
labelClassName="flex items-center justify-between gap-small px-small py-2 rounded-[0.625rem] bg-[#1F282C] w-[9.25rem] mr-medium transition-colors hover:bg-white/20"
childClassName="flex flex-col gap-small p-small rounded-[0.625rem] bg-[#00141D] border border-white/20"
offsetSize={12}
sameWidth
>
{tokenOptions.map((option) => (
<TokenOption key={option.symbol} selected={token} option={option} onSelect={onTokenChange} />
))}
</Select>
) : null}
</div>
);
}
const { styles, isMounted } = useTransitionStyles(context, {
initial: { transform: "translateY(-10px)", opacity: 0 },
open: { transform: "translateY(0)", opacity: 1 },
close: { transform: "translateY(-10px)", opacity: 0 },
});

function ChainOption({
selected,
option,
token,
onSelect = () => undefined,
}: {
selected: ChainConfig;
option: ChainConfig;
token: Token;
onSelect?: (chain: ChainConfig) => void;
}) {
const { balanceAll } = useApp();
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);

return (
<button
className="gap-small py-medium flex items-center justify-between px-5 transition-colors hover:bg-white/5 disabled:bg-white/10"
disabled={selected.id === option.id}
onClick={() => {
onSelect(option);
}}
>
<div className="gap-large flex items-center">
<img
width={30}
height={30}
alt="Chain"
src={getChainLogoSrc(option.logo)}
className="h-[1.875rem] w-[1.875rem] shrink-0 rounded-full"
/>
<span className="truncate text-sm font-bold text-white">{option.name}</span>
</div>

{balanceAll
.filter((b) => b.chain.id === option.id && b.token.category === token.category)
.slice(0, 1)
.map((b) => (
<span
className="truncate text-xs font-medium text-white/50"
key={`${b.chain.network}-${b.token.symbol}`}
>{`${formatBalance(b.balance, b.token.decimals, { precision: 6 })} ${b.token.symbol}`}</span>
))}
</button>
);
}

function TokenOption({
selected,
option,
onSelect = () => undefined,
}: {
selected: Token;
option: Token;
onSelect?: (token: Token) => void;
}) {
return (
<button
className="gap-small p-small flex items-center rounded-[0.625rem] transition-colors hover:bg-white/5 disabled:bg-white/10"
disabled={selected.symbol === option.symbol}
onClick={() => {
onSelect(option);
}}
>
<img
width={24}
height={24}
alt="Chain"
src={getTokenLogoSrc(option.logo)}
className="h-[1.5rem] w-[1.5rem] shrink-0 rounded-full"
/>
<span className="truncate text-sm font-bold text-white">{option.name}</span>
</button>
<>
<button
className={`gap-small mx-medium py-small group flex w-full items-center justify-between rounded-[0.625rem] transition-colors hover:bg-white/5 ${isOpen ? "bg-white/5" : ""}`}
ref={refs.setReference}
{...getReferenceProps()}
>
<div
className={`gap-medium flex items-center transition-transform group-hover:translate-x-2 ${isOpen ? "translate-x-2" : ""}`}
>
{label}
</div>
<div className={`transition-transform group-hover:-translate-x-2 ${isOpen ? "-translate-x-2" : ""}`}>
<img
style={{ transform: isOpen ? "rotateX(180deg)" : "rotateX(0)" }}
className="shrink-0 transition-transform"
src="images/caret-down.svg"
alt="Caret down"
width={16}
height={16}
/>
</div>
</button>
{isMounted && (
<FloatingPortal>
<div style={floatingStyles} ref={refs.setFloating} {...getFloatingProps()} className="z-20">
<div
className="py-medium rounded-large flex flex-col gap-2 border border-white/20 bg-[#00141D]"
onClick={() => setIsOpen(false)}
style={styles}
>
{children}
</div>
</div>
</FloatingPortal>
)}
</>
);
}
Loading
Loading