Skip to content

Commit

Permalink
Improve relayer register (#691)
Browse files Browse the repository at this point in the history
* Update transfer-chain-select styles

* Fix body overflow style after unmount modal

* Update modal background color

* Update modal border radius

* Update modal footer button font styles

* Update VersionSwitch styles

* Optimize relayer register ui and ux

* Fix eslint in lint-staged

* Fix lint warning
  • Loading branch information
JayJay1024 authored Apr 10, 2024
1 parent 0341a34 commit 2a77906
Show file tree
Hide file tree
Showing 16 changed files with 400 additions and 587 deletions.
4 changes: 2 additions & 2 deletions .lintstagedrc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const path = require("path");

const buildAppsEslintCommand = (filenames) =>
`npm run lint -w apps -- --fix --file ${filenames.map((f) => path.relative("packages/apps", f)).join(" --file ")}`;
`npm run lint -- --fix --file ${filenames.map((f) => path.relative("./", f)).join(" --file ")}`;

module.exports = {
"packages/apps/src/**/*.{js,jsx,ts,tsx}": [buildAppsEslintCommand],
"src/**/*.{js,jsx,ts,tsx}": [buildAppsEslintCommand],
"**/*.{js,jsx,ts,tsx,json}": "prettier --write",
};
8 changes: 5 additions & 3 deletions src/app/relayer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import RelayerProviderV3 from "@/providers/relayer-provider-v3";
import PageWrap from "@/ui/page-wrap";
import SegmentedTabs from "@/ui/segmented-tabs";
import VersionSwitch from "@/components/version-switch";
import { useState } from "react";
import { useCallback, useState } from "react";

type TabKey = "manage" | "register" | "overview";

export default function RelayerPage() {
const [activeTab, setActiveTab] = useState<TabKey>("manage");
const [version, setVersion] = useState<"v2" | "v3">("v3");

const handleManage = useCallback(() => setActiveTab("manage"), []);

return (
<PageWrap>
<div className="flex flex-col items-center gap-5">
Expand Down Expand Up @@ -46,11 +48,11 @@ export default function RelayerPage() {
children:
version === "v3" ? (
<RelayerProviderV3>
<RelayerRegisterV3 />
<RelayerRegisterV3 onManage={handleManage} />
</RelayerProviderV3>
) : (
<RelayerProvider>
<RelayerRegister />
<RelayerRegister onManage={handleManage} />
</RelayerProvider>
),
},
Expand Down
117 changes: 13 additions & 104 deletions src/components/balance-input.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { InputValue, Token } from "@/types";
import Input from "@/ui/input";
import InputAlert from "@/ui/input-alert";
import { formatBalance, getTokenLogoSrc } from "@/utils";
import Image from "next/image";
import { formatBalance } from "@/utils";
import { ChangeEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { parseUnits } from "viem";

Expand All @@ -13,60 +12,42 @@ enum ErrorCode {
}

interface Props {
placeholder?: string;
balance?: bigint;
max?: bigint;
min?: bigint;
compact?: boolean;
autoFocus?: boolean;
disabled?: boolean;
suffix?: "symbol";
enabledDynamicStyle?: boolean;
balance?: bigint;
placeholder?: string;
value: InputValue<bigint>;
token: Token | undefined;
tokenOptions?: Token[];
balanceLoading?: boolean;
onBalanceRefresh?: () => void;
onChange?: (value: InputValue<bigint>) => void;
onTokenChange?: (token: Token) => void;
}

export function BalanceInput({
placeholder,
balance,
max,
min,
compact,
autoFocus,
balance,
disabled,
suffix,
enabledDynamicStyle,
placeholder,
value,
token,
balanceLoading,
tokenOptions = [],
onBalanceRefresh = () => undefined,
onChange = () => undefined,
onTokenChange = () => undefined,
}: Props) {
const spanRef = useRef<HTMLSpanElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const balanceRef = useRef(balance);
const tokenRef = useRef(token);

const [dynamicStyle, setDynamicStyle] = useState("text-sm font-medium");
const [errorCode, setErrorCode] = useState<ErrorCode>();

const _placeholder = useMemo(() => {
if (token && compact) {
if (token?.decimals) {
if (max !== undefined) {
return `Max ${formatBalance(max, token.decimals)}`;
} else if (balance !== undefined) {
return `Balance ${formatBalance(balance, token.decimals)}`;
}
}
return placeholder ?? "Enter an amount";
}, [balance, max, placeholder, token, compact]);
}, [balance, max, placeholder, token?.decimals]);

const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(e) => {
Expand Down Expand Up @@ -121,104 +102,32 @@ export function BalanceInput({
}
}, [token, onChange]);

useEffect(() => {
if (enabledDynamicStyle) {
const inputWidth = inputRef.current?.clientWidth || 1;
const spanWidth = spanRef.current?.clientWidth || 0;
const percent = (spanWidth / inputWidth) * 100;
if (percent < 10) {
setDynamicStyle("text-[3.75rem] font-extralight");
} else if (percent < 20) {
setDynamicStyle("text-[3rem] font-light");
} else if (percent < 30) {
setDynamicStyle("text-[2.25rem] font-light");
} else if (percent < 40) {
setDynamicStyle("text-[1.875rem] font-normal");
} else if (percent < 50) {
setDynamicStyle("text-[1.5rem] font-normal");
} else if (percent < 60) {
setDynamicStyle("text-[1.25rem] font-medium");
} else {
setDynamicStyle("text-[1.125rem] font-medium");
}
}
}, [value.input, enabledDynamicStyle]);

return (
<div
className={`normal-input-wrap relative flex flex-col rounded-medium bg-inner px-small py-small lg:px-medium ${
compact ? "lg:py-medium" : ""
} ${value.valid ? "valid-input-wrap border-transparent" : "invalid-input-wrap"}`}
className={`normal-input-wrap relative ${
value.valid ? "valid-input-wrap border-transparent" : "invalid-input-wrap"
} rounded-xl bg-app-bg px-medium`}
>
<div className="flex items-center justify-between gap-small">
<div className="flex h-10 items-center justify-between gap-small text-sm font-semibold text-white lg:h-11">
<Input
placeholder={_placeholder}
className={`w-full rounded bg-transparent text-white transition-[font-size,font-weight,line-height] duration-300 ${
compact ? "" : "h-12"
} ${enabledDynamicStyle ? `leading-none ${dynamicStyle}` : "text-sm font-medium"}`}
className="w-full rounded bg-transparent"
onChange={handleChange}
ref={inputRef}
disabled={disabled}
value={value.input}
autoFocus={autoFocus}
/>

{compact ? (
suffix === "symbol" && token ? (
<span className="text-sm font-medium">{token.symbol}</span>
) : null
) : (
<div className="flex shrink-0 items-center gap-medium self-end">
{token ? (
<div className="flex shrink-0 items-center gap-small">
<Image width={32} height={32} alt="Token" src={getTokenLogoSrc(token.logo)} className="rounded-full" />
<span className="text-base font-medium text-white">{token.symbol}</span>
</div>
) : null}
{tokenOptions
.filter((t) => t.symbol !== token?.symbol)
.map((t) => (
<Image
key={t.symbol}
width={26}
height={26}
alt="Token"
src={getTokenLogoSrc(t.logo)}
className="rounded-full opacity-80 transition-transform duration-300 hover:cursor-pointer hover:opacity-100 active:-translate-x-1"
onClick={() => onTokenChange(t)}
/>
))}
</div>
)}
{token ? <span>{token.symbol}</span> : null}
</div>

{!compact && token ? (
<div className="flex items-center gap-small">
<span className="text-xs font-medium text-white/50">
Balance: {formatBalance(balance ?? 0n, token.decimals)}
</span>
<button
className={`rounded-full bg-white/20 p-[3px] opacity-50 transition hover:bg-white/20 hover:opacity-100 active:scale-95 ${
balanceLoading ? "animate-spin" : ""
}`}
onClick={onBalanceRefresh}
>
<Image alt="Refresh" width={14} height={14} src="/images/refresh.svg" />
</button>
</div>
) : null}

{errorCode === ErrorCode.INSUFFICIENT ? (
<InputAlert text="* Insufficient" />
) : errorCode === ErrorCode.REQUIRE_LESS ? (
<InputAlert text={`* Max: ${formatBalance(max ?? 0n, token?.decimals ?? 0, { precision: 6 })}`} />
) : errorCode === ErrorCode.REQUIRE_MORE ? (
<InputAlert text={`* Min: ${formatBalance(min ?? 0n, token?.decimals ?? 0, { precision: 6 })}`} />
) : null}

<span className="invisible fixed left-0 top-0 -z-50" ref={spanRef}>
{value.input}
</span>
</div>
);
}
Expand Down
99 changes: 34 additions & 65 deletions src/components/chain-select.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,69 @@
import { ChainConfig } from "@/types";
import Select from "@/ui/select";
import { getChainLogoSrc } from "@/utils";
import { Placement } from "@floating-ui/react";
import Image from "next/image";

interface Props {
options?: ChainConfig[];
value?: ChainConfig;
placeholder?: string;
compact?: boolean;
placement?: Placement;
className?: string;
onChange?: (value: ChainConfig | undefined) => void;
}

export default function ChainSelect({
value,
placeholder,
compact,
placement,
className,
options = [],
onChange = () => undefined,
}: Props) {
return (
<Select
labelClassName={`gap-small rounded-medium flex items-center justify-between transition active:translate-y-1 ${className}`}
childClassName={`bg-inner flex flex-col rounded-medium max-h-60 overflow-y-auto border border-component app-scrollbar ${
compact ? "py-small" : "p-medium"
}`}
sameWidth
clearable
label={
value ? (
<div className="flex items-center gap-medium truncate">
{compact ? null : (
<Image
alt="Chain"
width={32}
height={32}
src={getChainLogoSrc(value.logo)}
className="h-8 w-8 shrink-0 rounded-full"
/>
)}

<span className="truncate text-sm font-medium text-white">{value.name}</span>
<Image
alt="Chain"
width={22}
height={22}
src={getChainLogoSrc(value.logo)}
className="hidden shrink-0 rounded-full lg:inline"
/>
<span className="truncate text-sm font-semibold text-white">{value.name}</span>
</div>
) : undefined
}
placeholder={<span className="truncate text-sm font-medium text-slate-400">{placeholder}</span>}
placement={placement}
sameWidth={compact ? true : false}
clearable={compact ? true : false}
placeholder={<span className="truncate text-sm font-semibold text-slate-400">{placeholder}</span>}
labelClassName={`gap-small flex items-center justify-between ${className}`}
childClassName="bg-app-bg flex flex-col rounded-xl max-h-60 overflow-y-auto border border-white/20 app-scrollbar py-small"
onClear={() => onChange(undefined)}
>
{options.length ? (
compact ? (
options.map((option) => {
return (
<button
key={option.id}
onClick={() => onChange(option)}
className="flex items-center gap-medium px-large py-2 text-start transition-colors hover:bg-white/10"
>
<Image
width={20}
height={20}
alt="Chain logo"
src={getChainLogoSrc(option.logo)}
className="rounded-full"
/>
<span className="truncate text-sm font-medium text-white">{option.name}</span>
</button>
);
})
) : (
<div className="grid grid-cols-2 gap-small">
{options.map((option) => (
<button
key={option.id}
className="flex w-36 shrink-0 items-center gap-small truncate rounded-medium bg-component px-2 py-1 transition-colors hover:bg-white/20"
onClick={() => onChange(option)}
>
<Image
width={18}
height={18}
alt="Chain logo"
src={getChainLogoSrc(option.logo)}
className="rounded-full"
/>
<span className="truncate text-sm font-medium text-white">{option.name}</span>
</button>
))}
</div>
)
options.map((option) => {
return (
<button
key={option.id}
disabled={value?.id === option.id}
onClick={() => onChange(option)}
className="flex items-center gap-medium px-large py-medium text-start transition-colors hover:bg-white/5 disabled:bg-white/10"
>
<Image
width={20}
height={20}
alt="Chain logo"
src={getChainLogoSrc(option.logo)}
className="rounded-full"
/>
<span className="truncate text-sm font-semibold text-white">{option.name}</span>
</button>
);
})
) : (
<div className="px-large py-small">
<span className="text-sm text-white/50">No data</span>
<div className="inline-flex justify-center p-2">
<span className="text-sm font-semibold text-slate-400">No data</span>
</div>
)}
</Select>
Expand Down
Loading

0 comments on commit 2a77906

Please sign in to comment.