Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
reset
Browse files Browse the repository at this point in the history
codingki committed Jun 21, 2024
1 parent 5e33fba commit 5c5add6
Showing 114 changed files with 9,191 additions and 16 deletions.
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.99.0",
"@skip-router/core": "5.0.4",
"@skip-go/widget": "0.0.1-alpha.16",
"@skip-go/widget": "0.0.1-alpha.19",
"@solana/spl-token": "^0.4.1",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-wallets": "^0.19.31",
232 changes: 232 additions & 0 deletions src/components/AssetInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { BackspaceIcon } from "@heroicons/react/20/solid";
import { Asset } from "@skip-router/core";
import { BigNumber } from "bignumber.js";
import { MouseEventHandler, useMemo } from "react";
import { formatUnits } from "viem";

import { useAssets } from "@/context/assets";
import { useAnyDisclosureOpen } from "@/context/disclosures";
import { useAccount } from "@/hooks/useAccount";
import { useBalancesByChain } from "@/hooks/useBalancesByChain";
import { Chain } from "@/hooks/useChains";
import { formatPercent, formatUSD } from "@/utils/intl";
import { formatNumberWithCommas, formatNumberWithoutCommas } from "@/utils/number";
import { cn } from "@/utils/ui";

import AssetSelect from "./AssetSelect";
import ChainSelect from "./ChainSelect";
import { SimpleTooltip } from "./SimpleTooltip";
import { SpinnerIcon } from "./SpinnerIcon";

interface Props {
amount: string;
amountUSD?: string;
diffPercentage?: number;
onAmountChange?: (amount: string) => void;
onAmountMax?: MouseEventHandler<HTMLButtonElement>;
asset?: Asset;
onAssetChange?: (asset: Asset) => void;
chain?: Chain;
onChainChange?: (chain: Chain) => void;
chains: Chain[];
context: "source" | "destination";
isError?: string | boolean;
isLoading?: boolean;
}

function AssetInput({
amount,
amountUSD,
diffPercentage = 0,
onAmountChange,
onAmountMax,
asset,
onAssetChange,
chain,
chains,
onChainChange,
context,
isError,
isLoading,
}: Props) {
const { assetsByChainID, getNativeAssets } = useAssets();

const assets = useMemo(() => {
if (!chain) return getNativeAssets();
return assetsByChainID(chain.chainID);
}, [assetsByChainID, chain, getNativeAssets]);

const account = useAccount(chain?.chainID);

const isAnyDisclosureOpen = useAnyDisclosureOpen();

const { data: balances, isLoading: isBalancesLoading } = useBalancesByChain({
address: account?.address,
chain,
assets,
enabled: !isAnyDisclosureOpen && context === "source",
});

const selectedAssetBalance = useMemo(() => {
if (!asset || !balances) return "0";
return formatUnits(BigInt(balances[asset.denom] ?? "0"), asset.decimals ?? 6);
}, [asset, balances]);

const maxButtonDisabled = useMemo(() => {
return parseFloat(selectedAssetBalance) <= 0;
}, [selectedAssetBalance]);

return (
<div
className={cn(
"rounded-lg border border-neutral-200 p-4 transition-[border,shadow]",
"focus-within:border-neutral-300 focus-within:shadow-sm",
"hover:border-neutral-300 hover:shadow-sm",
!!isError && "border-red-400 focus-within:border-red-500 hover:border-red-500",
)}
>
<div className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 sm:gap-4">
<div>
<ChainSelect
chain={chain}
chains={chains}
onChange={onChainChange}
/>
</div>
<div>
<AssetSelect
asset={asset}
assets={assets}
balances={balances}
onChange={onAssetChange}
showChainInfo={!!chain}
isBalancesLoading={isBalancesLoading}
/>
</div>
</div>
<div className="relative isolate">
{isLoading && <SpinnerIcon className="absolute right-2 top-2 z-10 h-4 w-4 animate-spin text-neutral-300" />}
{amount && !isLoading && (
<button className="absolute right-2 top-2 z-10">
<BackspaceIcon
className="h-4 w-4 text-neutral-300 transition-colors hover:text-neutral-400"
onClick={() => onAmountChange?.("")}
/>
</button>
)}
<input
data-testid="amount"
className={cn(
"h-10 w-full text-3xl font-medium tabular-nums",
"placeholder:text-neutral-300 focus:outline-none",
isLoading && "animate-pulse text-neutral-500",
)}
type="text"
placeholder="0"
value={formatNumberWithCommas(amount)}
inputMode="numeric"
onChange={(e) => {
if (!onAmountChange) return;

let latest = e.target.value;

if (latest.match(/^[.,]/)) latest = `0.${latest}`; // Handle first character being a period or comma
latest = latest.replace(/^[0]{2,}/, "0"); // Remove leading zeros
latest = latest.replace(/[^\d.,]/g, ""); // Remove non-numeric and non-decimal characters
latest = latest.replace(/[.]{2,}/g, "."); // Remove multiple decimals
latest = latest.replace(/[,]{2,}/g, ","); // Remove multiple commas

onAmountChange?.(formatNumberWithoutCommas(latest));
}}
onKeyDown={(event) => {
if (!onAmountChange) return;

if (event.key === "Escape") {
if (event.currentTarget.selectionStart === event.currentTarget.selectionEnd) {
event.currentTarget.select();
}
return;
}

if (event.key === "ArrowUp" || event.key === "ArrowDown") {
let value = new BigNumber(formatNumberWithoutCommas(event.currentTarget.value) || "0");
if (event.key === "ArrowUp") {
event.preventDefault();
if (event.shiftKey) {
value = value.plus(10);
} else if (event.altKey || event.ctrlKey || event.metaKey) {
value = value.plus(0.1);
} else {
value = value.plus(1);
}
}
if (event.key === "ArrowDown") {
event.preventDefault();
if (event.shiftKey) {
value = value.minus(10);
} else if (event.altKey || event.ctrlKey || event.metaKey) {
value = value.minus(0.1);
} else {
value = value.minus(1);
}
}
if (value.isNegative()) {
value = new BigNumber(0);
}
onAmountChange(value.toString());
}
}}
/>
<div className="flex h-8 items-center space-x-2 tabular-nums">
<p className="text-sm tabular-nums text-neutral-400">
{amountUSD && Number(amountUSD) > 0 ? formatUSD(amountUSD) : null}
</p>
{amountUSD !== undefined && Number(amountUSD) > 0 && diffPercentage !== 0 && context === "destination" ? (
<p className={cn("text-sm tabular-nums", diffPercentage >= 0 ? "text-green-500" : "text-red-500")}>
({formatPercent(diffPercentage)})
</p>
) : null}
<div className="flex-grow" />
{context === "source" && account?.address && asset && (
<div className="flex animate-slide-left-and-fade items-center text-sm text-neutral-400">
<span className="mr-1">Balance:</span>{" "}
{isBalancesLoading ? (
<SpinnerIcon className="mr-2 h-4 w-4 animate-spin" />
) : (
<SimpleTooltip label={`${parseFloat(selectedAssetBalance).toString()} ${asset.recommendedSymbol}`}>
<div
className={cn(
"mr-2 max-w-[16ch] truncate tabular-nums",
"cursor-help underline decoration-dotted underline-offset-4",
)}
>
{parseFloat(selectedAssetBalance).toLocaleString("en-US", {
maximumFractionDigits: 4,
})}
</div>
</SimpleTooltip>
)}
<button
className={cn(
"rounded-md bg-[#FF486E] px-2 py-1 text-xs font-semibold uppercase text-white disabled:bg-red-200",
"transition-[transform,background] enabled:hover:rotate-2 enabled:hover:scale-110 disabled:cursor-not-allowed",
)}
disabled={maxButtonDisabled}
onClick={onAmountMax}
>
Max
</button>
</div>
)}
</div>
</div>
{typeof isError === "string" && (
<div className="mt-2 animate-slide-up-and-fade text-balance text-center text-xs font-medium text-red-500">
{isError}
</div>
)}
</div>
);
}

export default AssetInput;
Loading

0 comments on commit 5c5add6

Please sign in to comment.