Skip to content

Commit 34ec2ec

Browse files
authored
feat(explorer): add chain switch (#3705)
1 parent bd9e19b commit 34ec2ec

11 files changed

Lines changed: 230 additions & 52 deletions

File tree

.changeset/lovely-rats-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
Chain switching between supported networks is now accessible on the homepage and the world page.
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 51 additions & 0 deletions
Loading
Lines changed: 20 additions & 0 deletions
Loading
Lines changed: 31 additions & 0 deletions
Loading

packages/explorer/src/app/(explorer)/[chainName]/worlds/WorldsForm.tsx

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Command as CommandPrimitive } from "cmdk";
88
import { useState } from "react";
99
import { useForm } from "react-hook-form";
1010
import { zodResolver } from "@hookform/resolvers/zod";
11+
import { ChainSwitch } from "../../../../components/ChainSwitch";
1112
import { Button } from "../../../../components/ui/Button";
1213
import { Command, CommandGroup, CommandItem, CommandList } from "../../../../components/ui/Command";
1314
import { Form, FormControl, FormField, FormItem, FormMessage } from "../../../../components/ui/Form";
@@ -39,16 +40,16 @@ export function WorldsForm({ worlds, isLoading }: Props) {
3940
reValidateMode: "onChange",
4041
});
4142

42-
function onSubmit({ worldAddress }: z.infer<typeof formSchema>) {
43+
const onSubmit = ({ worldAddress }: z.infer<typeof formSchema>) => {
4344
router.push(getWorldUrl(chainName as string, worldAddress));
44-
}
45+
};
4546

46-
function onLuckyWorld() {
47+
const onLuckyWorld = () => {
4748
if (worlds.length > 0) {
4849
const luckyAddress = worlds[Math.floor(Math.random() * worlds.length)];
4950
router.push(getWorldUrl(chainName as string, luckyAddress as Address));
5051
}
51-
}
52+
};
5253

5354
const handleOpenOptions = () => {
5455
if (!open && worlds.length > 0) {
@@ -66,40 +67,46 @@ export function WorldsForm({ worlds, isLoading }: Props) {
6667
<Form {...form}>
6768
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
6869
<div>
69-
<FormField
70-
control={form.control}
71-
name="worldAddress"
72-
render={({ field }) => (
73-
<FormItem>
74-
<FormControl>
75-
<CommandPrimitive.Input
76-
asChild
77-
value={field.value}
78-
onValueChange={(value) => {
79-
field.onChange(value);
80-
}}
81-
onBlur={() => {
82-
field.onBlur();
83-
setOpen(false);
84-
}}
85-
onFocus={handleOpenOptions}
86-
placeholder={isLoading ? "Loading worlds..." : "Enter world address..."}
87-
// Need to manually trigger form submission as CommandPrimitive.Input captures Enter key events
88-
onKeyDown={(e) => {
89-
if (!open && e.key === "Enter") {
90-
e.preventDefault();
91-
form.handleSubmit(onSubmit)();
92-
}
93-
}}
94-
disabled={isLoading}
95-
>
96-
<Input className="h-12" />
97-
</CommandPrimitive.Input>
98-
</FormControl>
99-
<FormMessage className="uppercase" />
100-
</FormItem>
101-
)}
102-
/>
70+
<div className="flex items-center gap-x-2">
71+
<div className="w-[260px] flex-shrink-0">
72+
<FormField
73+
control={form.control}
74+
name="worldAddress"
75+
render={({ field }) => (
76+
<FormItem>
77+
<FormControl>
78+
<CommandPrimitive.Input
79+
asChild
80+
value={field.value}
81+
onValueChange={(value) => {
82+
field.onChange(value);
83+
}}
84+
onBlur={() => {
85+
field.onBlur();
86+
setOpen(false);
87+
}}
88+
onFocus={handleOpenOptions}
89+
placeholder={isLoading ? "Loading worlds..." : "Enter world address..."}
90+
// Need to manually trigger form submission as CommandPrimitive.Input captures Enter key events
91+
onKeyDown={(e) => {
92+
if (!open && e.key === "Enter") {
93+
e.preventDefault();
94+
form.handleSubmit(onSubmit)();
95+
}
96+
}}
97+
disabled={isLoading}
98+
>
99+
<Input className="h-12" />
100+
</CommandPrimitive.Input>
101+
</FormControl>
102+
<FormMessage className="uppercase" />
103+
</FormItem>
104+
)}
105+
/>
106+
</div>
107+
108+
<ChainSwitch className="w-full" />
109+
</div>
103110

104111
<div className="relative">
105112
<CommandList>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Image from "next/image";
2+
import { useParams, useRouter } from "next/navigation";
3+
import { anvil } from "viem/chains";
4+
import { useEnv } from "../app/(explorer)/providers/EnvProvider";
5+
import { supportedChains } from "../common";
6+
import { capitalize, cn } from "../utils";
7+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/Select";
8+
9+
type Props = {
10+
className?: string;
11+
size?: "default" | "sm" | "lg" | "icon";
12+
};
13+
14+
export function ChainSwitch({ className, size = "default" }: Props) {
15+
const router = useRouter();
16+
const { chainName } = useParams();
17+
const env = useEnv();
18+
const chainId = Number(env.CHAIN_ID);
19+
20+
const onChainChange = (chainName: string) => {
21+
router.push(`/${chainName}/worlds`);
22+
};
23+
24+
return (
25+
<Select value={chainName as string} onValueChange={onChainChange}>
26+
<SelectTrigger className={cn("w-auto", className)} size={size}>
27+
<SelectValue placeholder="Select chain" />
28+
</SelectTrigger>
29+
<SelectContent>
30+
{Object.entries(supportedChains)
31+
.filter(([, chain]) => (chainId === anvil.id ? true : chain.id !== anvil.id))
32+
.map(([name, chain]) => (
33+
<SelectItem key={name} value={name}>
34+
<div className="flex items-center gap-x-2 pr-2">
35+
<Image src={`/logos/${name}.svg`} alt={chain.name} width={24} height={24} />
36+
{chain.id === anvil.id ? "Local" : capitalize(name)}
37+
</div>
38+
</SelectItem>
39+
))}
40+
</SelectContent>
41+
</Select>
42+
);
43+
}

packages/explorer/src/components/LatestBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function LatestBlock() {
1313
});
1414

1515
return (
16-
<div className="inline-block w-[50px]">
16+
<div className="inline-block w-[80px]">
1717
{block ? (
1818
<div className="flex items-center justify-end gap-x-2 text-xs font-extrabold text-green-600">
1919
<span className="inline-block h-[8px] w-[8px] animate-pulse rounded-full bg-success"></span>

packages/explorer/src/components/Navigation.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useWorldAbiQuery } from "../app/(explorer)/queries/useWorldAbiQuery";
99
import { LatestBlock } from "../components/LatestBlock";
1010
import { Separator } from "../components/ui/Separator";
1111
import { cn } from "../utils";
12+
import { ChainSwitch } from "./ChainSwitch";
1213
import { ConnectButton } from "./ConnectButton";
1314

1415
function NavigationLink({ href, children }: { href: string; children: React.ReactNode }) {
@@ -53,6 +54,7 @@ export function Navigation() {
5354

5455
<div className="flex items-center gap-x-4">
5556
<LatestBlock />
57+
<ChainSwitch size="sm" />
5658
{!isReadOnly ? <ConnectButton /> : null}
5759
</div>
5860
</div>

packages/explorer/src/components/ui/Select.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
"use client";
22

33
import { Check, ChevronDown, ChevronUp } from "lucide-react";
4+
import { cva } from "class-variance-authority";
45
import * as React from "react";
56
import * as SelectPrimitive from "@radix-ui/react-select";
67
import { cn } from "../../utils";
78

9+
const selectTriggerVariants = cva(
10+
cn(
11+
"flex items-center justify-between",
12+
"rounded-md border border-input bg-background ring-offset-background",
13+
"placeholder:text-muted-foreground",
14+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
15+
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
16+
),
17+
{
18+
variants: {
19+
size: {
20+
default: "h-12 w-full px-3 py-2 text-sm",
21+
sm: "h-9 w-full px-3 py-1 text-sm",
22+
lg: "h-11 w-full px-4 py-2 text-base",
23+
icon: "h-10 w-10 p-2",
24+
},
25+
},
26+
defaultVariants: {
27+
size: "default",
28+
},
29+
},
30+
);
31+
832
const Select = SelectPrimitive.Root;
933

1034
const SelectGroup = SelectPrimitive.Group;
@@ -13,19 +37,9 @@ const SelectValue = SelectPrimitive.Value;
1337

1438
const SelectTrigger = React.forwardRef<
1539
React.ElementRef<typeof SelectPrimitive.Trigger>,
16-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
17-
>(({ className, children, ...props }, ref) => (
18-
<SelectPrimitive.Trigger
19-
ref={ref}
20-
className={cn(
21-
"flex h-10 w-full items-center justify-between px-3 py-2 text-sm",
22-
"rounded-md border border-input bg-background ring-offset-background",
23-
"placeholder:text-muted-foreground",
24-
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
25-
className,
26-
)}
27-
{...props}
28-
>
40+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { size?: "default" | "sm" | "lg" | "icon" }
41+
>(({ className, children, size, ...props }, ref) => (
42+
<SelectPrimitive.Trigger ref={ref} className={cn(selectTriggerVariants({ size, className }))} {...props}>
2943
{children}
3044
<SelectPrimitive.Icon asChild>
3145
<ChevronDown className="h-4 w-4 opacity-50" />

0 commit comments

Comments
 (0)