-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Kevin Ingersoll <[email protected]>
- Loading branch information
Showing
9 changed files
with
239 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@latticexyz/explorer": patch | ||
--- | ||
|
||
Each chain's home page now lets you find and pick a world to explore. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
packages/explorer/src/app/(explorer)/[chainName]/worlds/WorldsForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
"use client"; | ||
|
||
import Image from "next/image"; | ||
import { useParams, useRouter } from "next/navigation"; | ||
import { Address, isAddress } from "viem"; | ||
import * as z from "zod"; | ||
import { Command as CommandPrimitive } from "cmdk"; | ||
import { useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { Button } from "../../../../components/ui/Button"; | ||
import { Command, CommandGroup, CommandItem, CommandList } from "../../../../components/ui/Command"; | ||
import { Form, FormControl, FormField, FormItem, FormMessage } from "../../../../components/ui/Form"; | ||
import { Input } from "../../../../components/ui/Input"; | ||
import mudLogo from "../../icon.svg"; | ||
import { getWorldUrl } from "../../utils/getWorldUrl"; | ||
|
||
const formSchema = z.object({ | ||
worldAddress: z | ||
.string() | ||
.refine((value) => isAddress(value), { | ||
message: "Invalid world address", | ||
}) | ||
.transform((value): Address => value as Address), | ||
}); | ||
|
||
export function WorldsForm({ worlds }: { worlds: Address[] }) { | ||
const router = useRouter(); | ||
const { chainName } = useParams(); | ||
const [open, setOpen] = useState(false); | ||
|
||
const form = useForm<z.infer<typeof formSchema>>({ | ||
resolver: zodResolver(formSchema), | ||
reValidateMode: "onChange", | ||
}); | ||
|
||
function onSubmit({ worldAddress }: z.infer<typeof formSchema>) { | ||
router.push(getWorldUrl(chainName as string, worldAddress)); | ||
} | ||
|
||
function onLuckyWorld() { | ||
if (worlds.length > 0) { | ||
const luckyAddress = worlds[Math.floor(Math.random() * worlds.length)]; | ||
router.push(getWorldUrl(chainName as string, luckyAddress)); | ||
} | ||
} | ||
|
||
return ( | ||
<div className="mx-auto flex min-h-screen w-[450px] flex-col items-center justify-center p-4"> | ||
<h1 className="flex items-center gap-6 self-start font-mono text-4xl font-bold uppercase"> | ||
<Image src={mudLogo} alt="MUD logo" width={48} height={48} /> Worlds Explorer | ||
</h1> | ||
|
||
<Command className="mt-6 overflow-visible bg-transparent"> | ||
<Form {...form}> | ||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | ||
<div> | ||
<FormField | ||
control={form.control} | ||
name="worldAddress" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormControl> | ||
<CommandPrimitive.Input | ||
asChild | ||
value={field.value} | ||
onValueChange={(value) => { | ||
field.onChange(value); | ||
}} | ||
onBlur={() => { | ||
field.onBlur(); | ||
setOpen(false); | ||
}} | ||
onFocus={() => setOpen(true)} | ||
placeholder="Enter world address..." | ||
> | ||
<Input className="h-12" /> | ||
</CommandPrimitive.Input> | ||
</FormControl> | ||
<FormMessage className="uppercase" /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<div className="relative"> | ||
<CommandList> | ||
{open ? ( | ||
<div className="absolute top-3 z-10 max-h-[200px] w-full overflow-y-auto rounded-md border bg-popover text-popover-foreground outline-none animate-in"> | ||
<CommandGroup> | ||
{worlds?.map((world) => { | ||
return ( | ||
<CommandItem | ||
key={world} | ||
value={world} | ||
onMouseDown={(event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
}} | ||
onSelect={(value) => { | ||
form.setValue("worldAddress", value as Address, { | ||
shouldValidate: true, | ||
}); | ||
setOpen(false); | ||
}} | ||
className="cursor-pointer font-mono" | ||
> | ||
{world} | ||
</CommandItem> | ||
); | ||
})} | ||
</CommandGroup> | ||
</div> | ||
) : null} | ||
</CommandList> | ||
</div> | ||
</div> | ||
|
||
<div className="flex w-full items-center gap-x-2"> | ||
<Button type="submit" className="flex-1 uppercase" variant="default"> | ||
Explore the world | ||
</Button> | ||
<Button | ||
className="flex-1 uppercase" | ||
variant="secondary" | ||
onClick={onLuckyWorld} | ||
disabled={worlds.length === 0} | ||
> | ||
I'm feeling lucky | ||
</Button> | ||
</div> | ||
</form> | ||
</Form> | ||
</Command> | ||
</div> | ||
); | ||
} |
60 changes: 54 additions & 6 deletions
60
packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,63 @@ | ||
import { notFound, redirect } from "next/navigation"; | ||
import { headers } from "next/headers"; | ||
import { redirect } from "next/navigation"; | ||
import { Address } from "viem"; | ||
import { supportedChains, validateChainName } from "../../../../common"; | ||
import { indexerForChainId } from "../../utils/indexerForChainId"; | ||
import { WorldsForm } from "./WorldsForm"; | ||
|
||
export const dynamic = "force-dynamic"; | ||
type ApiResponse = { | ||
items: { | ||
address: { | ||
hash: Address; | ||
}; | ||
}[]; | ||
}; | ||
|
||
async function fetchWorlds(chainName: string): Promise<Address[]> { | ||
validateChainName(chainName); | ||
|
||
const chain = supportedChains[chainName]; | ||
const indexer = indexerForChainId(chain.id); | ||
let worldsApiUrl: string | null = null; | ||
|
||
if (indexer.type === "sqlite") { | ||
const headersList = headers(); | ||
const host = headersList.get("host") || ""; | ||
const protocol = headersList.get("x-forwarded-proto") || "http"; | ||
const baseUrl = `${protocol}://${host}`; | ||
worldsApiUrl = `${baseUrl}/api/sqlite-indexer/worlds`; | ||
} else { | ||
const blockExplorerUrl = chain.blockExplorers?.default.url; | ||
if (blockExplorerUrl) { | ||
worldsApiUrl = `${blockExplorerUrl}/api/v2/mud/worlds`; | ||
} | ||
} | ||
|
||
if (!worldsApiUrl) { | ||
return []; | ||
} | ||
|
||
try { | ||
const response = await fetch(worldsApiUrl); | ||
const data: ApiResponse = await response.json(); | ||
return data.items.map((world) => world.address.hash); | ||
} catch (error) { | ||
console.error(error); | ||
return []; | ||
} | ||
} | ||
|
||
type Props = { | ||
params: { | ||
chainName: string; | ||
}; | ||
}; | ||
|
||
export default function WorldsPage({ params }: Props) { | ||
const worldAddress = process.env.WORLD_ADDRESS; | ||
if (worldAddress) return redirect(`/${params.chainName}/worlds/${worldAddress}`); | ||
return notFound(); | ||
export default async function WorldsPage({ params }: Props) { | ||
const worlds = await fetchWorlds(params.chainName); | ||
if (worlds.length === 1) { | ||
return redirect(`/${params.chainName}/worlds/${worlds[0]}`); | ||
} | ||
|
||
return <WorldsForm worlds={worlds} />; | ||
} |
26 changes: 26 additions & 0 deletions
26
packages/explorer/src/app/(explorer)/api/sqlite-indexer/worlds/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Address } from "viem"; | ||
import { getDatabase } from "../../utils/getDatabase"; | ||
|
||
export const dynamic = "force-dynamic"; | ||
|
||
type Row = { | ||
address: Address; | ||
}; | ||
|
||
type SqliteTable = Row[] | undefined; | ||
|
||
export async function GET() { | ||
try { | ||
const db = getDatabase(); | ||
const data = (await db?.prepare("SELECT DISTINCT address FROM __mudStoreTables").all()) as SqliteTable; | ||
const items = data?.map((row) => ({ | ||
address: { | ||
hash: row.address, | ||
}, | ||
})); | ||
return Response.json({ items: items || [] }); | ||
} catch (error: unknown) { | ||
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; | ||
return Response.json({ error: errorMessage }, { status: 400 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
import { useParams } from "next/navigation"; | ||
import { Address } from "viem"; | ||
import { getWorldUrl } from "../utils/getWorldUrl"; | ||
|
||
export function useWorldUrl() { | ||
const params = useParams(); | ||
const { chainName, worldAddress } = params; | ||
return (page: string) => `/${chainName}/worlds/${worldAddress}/${page}`; | ||
return (page: string) => `${getWorldUrl(chainName as string, worldAddress as Address)}/${page}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Address } from "viem"; | ||
|
||
export function getWorldUrl(chainName: string, worldAddress: Address) { | ||
return `/${chainName}/worlds/${worldAddress}`; | ||
} |
Oops, something went wrong.