Add new ChainConnect
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { deleteLocalChainFromStorage } from "@/context/ChainsContext/storage";
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import { CheckCircle, ChevronsUpDown, ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import ButtonWithConfirm from "../inputs/ButtonWithConfirm";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";
|
||||
|
||||
interface ChainItemProps {
|
||||
readonly chain: ChainInfo;
|
||||
readonly simplify?: boolean;
|
||||
}
|
||||
|
||||
export default function ChainDigest({ chain, simplify }: ChainItemProps) {
|
||||
const { chain: connectedChain, chains } = useChains();
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="column flex flex-wrap items-center gap-2">
|
||||
<Avatar className="overflow-visible">
|
||||
{!simplify && connectedChain.registryName === chain.registryName ? (
|
||||
<CheckCircle
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-6px",
|
||||
left: "-6px",
|
||||
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
|
||||
borderRadius: "50%",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
|
||||
color: "rgb(20 83 45 / var(--tw-text-opacity))",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<AvatarImage src={chain.logo} alt={`${chain.chainDisplayName} logo`} className="h-auto" />
|
||||
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<h4 className="text-sm font-semibold">{chain.chainDisplayName}</h4>
|
||||
<Badge>{chain.chainId}</Badge>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold">Fee token: {chain.displayDenom}</h4>
|
||||
<Collapsible className="w-[350px] space-y-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
{!simplify && chain.nodeAddresses.length > 1 ? (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button size="sm" className="p-1">
|
||||
<ChevronsUpDown className="black h-4 w-4" />
|
||||
<span className="sr-only">Toggle</span>
|
||||
<h4 className="text-sm font-semibold">RPC endpoints:</h4>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
) : (
|
||||
<h4 className="text-sm font-semibold">RPC endpoint:</h4>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border px-2 py-1 font-mono text-sm">
|
||||
{chain.nodeAddress || chain.nodeAddresses[0]}
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
{chain.nodeAddresses
|
||||
.filter(
|
||||
(address, _, nodeAddresses) =>
|
||||
(chain.nodeAddress && address !== chain.nodeAddress) ||
|
||||
(!chain.nodeAddress && address !== nodeAddresses[0]),
|
||||
)
|
||||
.map((address) => (
|
||||
<div key={address} className="rounded-md border px-2 py-1 font-mono text-sm">
|
||||
{address}
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{!simplify && chains.localnets.has(chain.registryName) ? (
|
||||
<div className="flex justify-center pt-2">
|
||||
<ButtonWithConfirm
|
||||
onClick={() => {
|
||||
deleteLocalChainFromStorage(chain.registryName, chains);
|
||||
}}
|
||||
text="Delete custom chain"
|
||||
confirmText="Confirm deletion?"
|
||||
disabled={connectedChain.registryName === chain.registryName}
|
||||
/>
|
||||
</div>
|
||||
) : !simplify ? (
|
||||
<div className="flex items-center pt-2 hover:cursor-pointer">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
<Link
|
||||
href={`https://github.com/cosmos/chain-registry/tree/master/${
|
||||
chains.testnets.has(chain.registryName) ? `testnets/` : ""
|
||||
}${chain.registryName}`}
|
||||
target="_blank"
|
||||
className="text-xs underline"
|
||||
>
|
||||
Check it out on the registry
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { setNewConnection } from "@/context/ChainsContext/helpers";
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { CommandItem } from "../ui/command";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card";
|
||||
import ChainDigest from "./ChainDigest";
|
||||
|
||||
interface ChainItemProps {
|
||||
readonly chain: ChainInfo;
|
||||
readonly hoverCardElementBoundary: HTMLDivElement | null;
|
||||
}
|
||||
|
||||
export default function ChainItem({ chain, hoverCardElementBoundary }: ChainItemProps) {
|
||||
const { chain: connectedChain, chainsDispatch } = useChains();
|
||||
|
||||
return (
|
||||
<HoverCard key={chain.registryName} openDelay={300}>
|
||||
<HoverCardTrigger asChild>
|
||||
<CommandItem
|
||||
value={chain.registryName}
|
||||
onSelect={
|
||||
connectedChain.registryName === chain.registryName
|
||||
? () => {}
|
||||
: () => {
|
||||
setNewConnection(chainsDispatch, { action: "confirm", chain });
|
||||
}
|
||||
}
|
||||
className={cn(
|
||||
"group relative inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border-2 border-white p-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4 focus:ring-red-100 aria-selected:text-gray-900",
|
||||
connectedChain.registryName === chain.registryName
|
||||
? "cursor-not-allowed border-green-600 bg-green-300 text-green-900 aria-selected:bg-green-300"
|
||||
: "transparent cursor-pointer border-white",
|
||||
)}
|
||||
>
|
||||
{connectedChain.registryName === chain.registryName ? (
|
||||
<CheckCircle
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-6px",
|
||||
left: "-6px",
|
||||
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
|
||||
borderRadius: "50%",
|
||||
width: "25px",
|
||||
height: "25px",
|
||||
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
|
||||
color: "rgb(20 83 45 / var(--tw-text-opacity))",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src={chain.logo}
|
||||
alt={`${chain.chainDisplayName} logo`}
|
||||
className="h-auto"
|
||||
/>
|
||||
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
{chain.registryName}
|
||||
</CommandItem>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-auto bg-fuchsia-900"
|
||||
collisionBoundary={hoverCardElementBoundary}
|
||||
>
|
||||
<ChainDigest chain={chain} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import { CommandGroup } from "../ui/command";
|
||||
import ChainItem from "./ChainItem";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface ChainsGroupProps {
|
||||
readonly chains: readonly ChainInfo[];
|
||||
readonly heading: string;
|
||||
readonly emptyMsg: string;
|
||||
}
|
||||
|
||||
export default function ChainsGroup({ chains, heading, emptyMsg }: ChainsGroupProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<CommandGroup
|
||||
heading={heading}
|
||||
className="[&_[cmdk-group-heading]]:my-3 [&_[cmdk-group-heading]]:text-base [&_[cmdk-group-heading]]:leading-[0] [&_[cmdk-group-heading]]:text-white [&_[cmdk-group-heading]]:underline"
|
||||
>
|
||||
{chains.length ? (
|
||||
<div ref={containerRef} className="flex flex-wrap gap-2">
|
||||
{chains.map((chain) => (
|
||||
<ChainItem
|
||||
key={chain.registryName}
|
||||
chain={chain}
|
||||
hoverCardElementBoundary={containerRef.current}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="ml-3 max-w-none">{emptyMsg}</p>
|
||||
)}
|
||||
</CommandGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { getRecentChainsFromStorage } from "@/context/ChainsContext/storage";
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { Command, CommandEmpty, CommandInput, CommandList, CommandSeparator } from "../ui/command";
|
||||
import ChainsGroup from "./ChainsGroup";
|
||||
|
||||
export default function ChooseChain() {
|
||||
const { chains } = useChains();
|
||||
const [recentChains, setRecentChains] = useState<readonly ChainInfo[]>([]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const newRecentChains = getRecentChainsFromStorage(chains);
|
||||
setRecentChains(newRecentChains);
|
||||
}, [chains]);
|
||||
|
||||
return (
|
||||
<Command
|
||||
className="bg-fuchsia-900 text-white"
|
||||
style={
|
||||
{
|
||||
"--accent": "0, 100%, 100%",
|
||||
"--border": "0, 100%, 100%",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<CommandInput placeholder="Type a chain name or id…" />
|
||||
<div className="overflow-x-auto overflow-y-auto">
|
||||
<CommandList className="max-h-full overflow-x-hidden overflow-y-visible">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<ChainsGroup
|
||||
chains={recentChains}
|
||||
heading="Recently used chains:"
|
||||
emptyMsg="No recent chains found."
|
||||
/>
|
||||
<CommandSeparator className="m-2" />
|
||||
<ChainsGroup
|
||||
chains={Array.from(chains.localnets.values())}
|
||||
heading="Custom chains:"
|
||||
emptyMsg={`No custom chains found. You can add one on the "Custom chain" tab.`}
|
||||
/>
|
||||
<CommandSeparator className="m-2" />
|
||||
<ChainsGroup
|
||||
chains={Array.from(chains.mainnets.values())}
|
||||
heading="Mainnets:"
|
||||
emptyMsg="No mainnets chains found."
|
||||
/>
|
||||
<CommandSeparator className="m-2" />
|
||||
<ChainsGroup
|
||||
chains={Array.from(chains.testnets.values())}
|
||||
heading="Testnets:"
|
||||
emptyMsg="No testnets chains found."
|
||||
/>
|
||||
</CommandList>
|
||||
</div>
|
||||
</Command>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { setChain, setNewConnection } from "@/context/ChainsContext/helpers";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "../ui/button";
|
||||
import ChainDigest from "./ChainDigest";
|
||||
|
||||
interface ConfirmConnectionProps {
|
||||
readonly closeDialog: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmConnection({ closeDialog }: ConfirmConnectionProps) {
|
||||
const router = useRouter();
|
||||
const { chain, newConnection, chainsDispatch } = useChains();
|
||||
|
||||
if (newConnection.action !== "confirm") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
Disconnect from "{chain.registryName}" and connect to "{newConnection.chain.registryName}"?
|
||||
</h3>
|
||||
<p className="max-w-none">
|
||||
You will be redirected to the homepage and any filled form will be lost
|
||||
</p>
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-around gap-4"
|
||||
style={{ "--border": "0, 100%, 100%" } as React.CSSProperties}
|
||||
>
|
||||
<div className="rounded-md border border-white p-4">
|
||||
<ChainDigest chain={chain} simplify />
|
||||
</div>
|
||||
<div className="rounded-md border border-white p-4">
|
||||
<ChainDigest chain={newConnection.chain} simplify />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
setNewConnection(chainsDispatch, { ...newConnection, action: "edit" });
|
||||
}}
|
||||
>
|
||||
Edit chain
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
setChain(chainsDispatch, newConnection.chain);
|
||||
closeDialog();
|
||||
router.push("/");
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { setNewConnection } from "@/context/ChainsContext/helpers";
|
||||
import { RegistryAsset } from "@/types/chainRegistry";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
const JsonEditor = dynamic(() => import("../inputs/JsonEditor"), { ssr: false });
|
||||
|
||||
export default function CustomChainForm() {
|
||||
const { chain, chains, newConnection, chainsDispatch } = useChains();
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
localRegistryName: z
|
||||
.string({ required_error: "Local registry name is required" })
|
||||
.refine((val) => !chains.mainnets.has(val) && !chains.testnets.has(val), {
|
||||
message: "Name already exists in remote registry",
|
||||
}),
|
||||
chainName: z.string({ required_error: "Chain name is required" }),
|
||||
chainId: z.string({ required_error: "Chain ID is required" }),
|
||||
baseDenom: z.string({ required_error: "Base denom is required" }),
|
||||
displayDenom: z.string({ required_error: "Display denom is required" }),
|
||||
denomExponent: z.string({ required_error: "Denom exponent is required" }),
|
||||
bech32Prefix: z.string({ required_error: "Address prefix is required" }),
|
||||
gasPrice: z.string({ required_error: "Gas price is required" }),
|
||||
rpcNodes: z.string({ required_error: "Comma separated rpc nodes are required" }),
|
||||
explorerLink: z.string({ required_error: "Explorer url is required" }),
|
||||
logo: z.string({ required_error: "Logo url is required" }),
|
||||
assets: z.string({ required_error: "Assets json is required" }),
|
||||
})
|
||||
.required();
|
||||
|
||||
const defaultChain = newConnection.chain ?? chain;
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
localRegistryName: defaultChain.registryName,
|
||||
chainName: defaultChain.chainDisplayName,
|
||||
chainId: defaultChain.chainId,
|
||||
baseDenom: defaultChain.denom,
|
||||
displayDenom: defaultChain.displayDenom,
|
||||
denomExponent: String(defaultChain.displayDenomExponent),
|
||||
bech32Prefix: defaultChain.addressPrefix,
|
||||
gasPrice: defaultChain.gasPrice,
|
||||
rpcNodes: defaultChain.nodeAddresses.join(", "),
|
||||
explorerLink: defaultChain.explorerLink,
|
||||
logo: defaultChain.logo,
|
||||
assets: JSON.stringify(defaultChain.assets),
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(chainFromForm: z.infer<typeof formSchema>) {
|
||||
const rpcNodes = chainFromForm.rpcNodes.split(", ");
|
||||
|
||||
setNewConnection(chainsDispatch, {
|
||||
action: "confirm",
|
||||
chain: {
|
||||
registryName: chainFromForm.localRegistryName,
|
||||
logo: chainFromForm.logo,
|
||||
chainId: chainFromForm.chainId,
|
||||
chainDisplayName: chainFromForm.chainName,
|
||||
nodeAddress: "",
|
||||
nodeAddresses: rpcNodes,
|
||||
denom: chainFromForm.baseDenom,
|
||||
displayDenom: chainFromForm.displayDenom,
|
||||
displayDenomExponent: Number(chainFromForm.denomExponent),
|
||||
assets: JSON.parse(chainFromForm.assets) as RegistryAsset[],
|
||||
gasPrice: chainFromForm.gasPrice,
|
||||
addressPrefix: chainFromForm.bech32Prefix,
|
||||
explorerLink: chainFromForm.explorerLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField
|
||||
name="localRegistryName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Local Registry Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mynetwork" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
A unique key to store this chain on your local registry
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="chainName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chain Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Network" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="chainId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chain ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-net-4" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="baseDenom"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Base Denom</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="umycoin" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="displayDenom"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Display Denom</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="MYCOIN" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="denomExponent"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Denom Exponent</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="6" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="bech32Prefix"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address Prefix</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="mynet" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Needs to be bech32</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="gasPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gas Price</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="0.04umycoin" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="explorerLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Explorer Link</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="url" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>with {"'${txHash}'"} included</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
name="rpcNodes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4">
|
||||
<FormLabel>RPC nodes</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="url1, url2, …, urln" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Can be one or more, separated by commas</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="logo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Logo URI</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="logo" className="border-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="assets"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4">
|
||||
<FormLabel>Assets</FormLabel>
|
||||
<FormControl>
|
||||
<div>
|
||||
<JsonEditor
|
||||
content={{ text: field.value }}
|
||||
onChange={(newMsgContent) => {
|
||||
field.onChange("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="mt-4">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { DialogTrigger } from "../ui/dialog";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function ChainHeader() {
|
||||
const { chain } = useChains();
|
||||
|
||||
return isChainInfoFilled(chain) ? (
|
||||
<>
|
||||
<Avatar>
|
||||
<AvatarImage src={chain.logo} alt={`${chain.chainDisplayName} logo`} />
|
||||
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<h1>{chain.chainDisplayName} Multisig</h1>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="h-12 w-12 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-[250px]" />
|
||||
<Skeleton className="h-4 w-[200px]" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DialogButton() {
|
||||
const showChainSelect = process.env.NEXT_PUBLIC_MULTICHAIN?.toLowerCase() === "true";
|
||||
|
||||
return showChainSelect ? (
|
||||
<DialogTrigger>
|
||||
<div className="group relative m-1 inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border-2 border-white p-2.5 text-sm font-medium text-white hover:bg-white hover:text-gray-900 focus:outline-none focus:ring-4 focus:ring-red-100">
|
||||
<ChainHeader />
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<div className="group relative m-1 inline-flex items-center justify-center gap-2 p-2.5 text-sm font-medium text-white">
|
||||
<ChainHeader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ComponentProps } from "react";
|
||||
import { TabsTrigger } from "../ui/tabs";
|
||||
|
||||
export default function TabButton({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof TabsTrigger>) {
|
||||
return (
|
||||
<TabsTrigger
|
||||
value={value}
|
||||
className={cn(
|
||||
"border-2 border-solid border-white bg-transparent text-white data-[state=active]:bg-white data-[state=active]:text-gray-900",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</TabsTrigger>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { setNewConnection } from "@/context/ChainsContext/helpers";
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader } from "../ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList } from "../ui/tabs";
|
||||
import ChooseChain from "./ChooseChain";
|
||||
import ConfirmConnection from "./ConfirmConnection";
|
||||
import CustomChainForm from "./CustomChainForm";
|
||||
import DialogButton from "./DialogButton";
|
||||
import TabButton from "./TabButton";
|
||||
|
||||
const tabs = { choose: "choose", custom: "custom" };
|
||||
|
||||
export default function ChainConnect() {
|
||||
const { newConnection, chainsDispatch } = useChains();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen(open);
|
||||
setNewConnection(chainsDispatch, { action: "edit" });
|
||||
}}
|
||||
>
|
||||
<DialogButton />
|
||||
<DialogContent
|
||||
className={"max-h-[75%] max-w-[75%] overflow-y-auto bg-fuchsia-900"}
|
||||
style={newConnection.action === "confirm" ? { width: "auto" } : {}}
|
||||
>
|
||||
{newConnection.action === "confirm" ? (
|
||||
<ConfirmConnection closeDialog={() => setDialogOpen(false)} />
|
||||
) : (
|
||||
<Tabs defaultValue={newConnection.chain ? tabs.custom : tabs.choose}>
|
||||
<DialogHeader>
|
||||
<TabsList className="justify-start gap-2 bg-transparent">
|
||||
<TabButton value={tabs.choose}>Choose chain</TabButton>
|
||||
<TabButton value={tabs.custom}>Custom chain</TabButton>
|
||||
</TabsList>
|
||||
</DialogHeader>
|
||||
<TabsContent value={tabs.choose}>
|
||||
<ChooseChain />
|
||||
</TabsContent>
|
||||
<TabsContent value={tabs.custom}>
|
||||
<CustomChainForm />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user