Skip to content

Commit c5dfad8

Browse files
committed
[TOOL-3298] Playground: Add Insight playground (#6185)
<!-- start pr-codex --> ## PR-Codex overview This PR introduces several enhancements and new features to the `playground-web` application, including a new `Spinner` component, improved layout for the `APIHeader`, and various updates to forms, UI components, and hooks for better functionality and usability. ### Detailed summary - Added `isProd` export in `env.ts`. - Introduced `Spinner` component in `Spinner.tsx` with CSS animations. - Updated `Layout` to include `APIHeader` with title and links. - Enhanced `RenderCode` to support new scrollable container class. - Added `Anchor` component with improved link handling. - Modified `CodeClient` to accept new props for scrollable container. - Implemented `useShowMore` hook for dynamic item display. - Updated `tailwind.config.ts` for new color schemes. - Refactored `AppSidebar` to accept dynamic links. - Introduced `fetchAllBlueprints` function for sidebar link generation. - Added `Alert`, `Tooltip`, and `Table` components with improved styling. - Enhanced `SelectWithSearch` for better option handling and search functionality. - Updated various components to use client-side rendering (`"use client"`). - Improved error handling and loading states in image components. - Added new utility functions for better link and chain handling. > The following files were skipped due to too many changes: `apps/playground-web/src/components/ui/select.tsx`, `apps/playground-web/src/app/insight/[blueprint_slug]/blueprint-playground.client.tsx`, `pnpm-lock.yaml` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 9663079 commit c5dfad8

38 files changed

+2614
-314
lines changed

apps/playground-web/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"dependencies": {
1717
"@abstract-foundation/agw-client": "^1.4.0",
1818
"@abstract-foundation/agw-react": "^1.5.0",
19+
"@hookform/resolvers": "^3.9.1",
1920
"@radix-ui/react-accordion": "^1.2.2",
2021
"@radix-ui/react-checkbox": "^1.1.3",
2122
"@radix-ui/react-dialog": "1.1.5",
@@ -38,16 +39,19 @@
3839
"next": "15.1.6",
3940
"next-themes": "^0.4.4",
4041
"nextjs-toploader": "^1.6.12",
42+
"openapi-types": "^12.1.3",
4143
"prettier": "3.3.3",
4244
"react": "19.0.0",
4345
"react-dom": "19.0.0",
46+
"react-hook-form": "7.54.2",
4447
"react-pick-color": "^2.0.0",
4548
"server-only": "^0.0.1",
4649
"shiki": "1.27.0",
4750
"tailwind-merge": "^2.6.0",
4851
"thirdweb": "workspace:*",
4952
"timeago.js": "^4.0.2",
50-
"use-debounce": "^10.0.4"
53+
"use-debounce": "^10.0.4",
54+
"zod": "3.24.1"
5155
},
5256
"devDependencies": {
5357
"@types/node": "22.13.0",
51.1 KB
Binary file not shown.

apps/playground-web/src/app/AppSidebar.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import thirdwebIconSrc from "@/../public/thirdweb.svg";
2-
import { Sidebar } from "@/components/ui/sidebar";
2+
import { Sidebar, type SidebarLink } from "@/components/ui/sidebar";
33
import Image from "next/image";
44
import Link from "next/link";
55
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
6-
import { navLinks } from "./navLinks";
76
import { otherLinks } from "./otherLinks";
87

9-
export function AppSidebar() {
8+
export function AppSidebar(props: {
9+
links: SidebarLink[];
10+
}) {
1011
return (
1112
<div className="z-10 hidden h-dvh w-[300px] flex-col border-border/50 border-r-2 xl:flex">
1213
<div className="border-b px-6 py-5">
@@ -23,7 +24,7 @@ export function AppSidebar() {
2324
className="grow pr-4 pl-6"
2425
scrollableClassName="max-h-full pt-6"
2526
>
26-
<Sidebar links={navLinks} />
27+
<Sidebar links={props.links} />
2728
</ScrollShadow>
2829
</div>
2930

apps/playground-web/src/app/MobileHeader.tsx

+42-39
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import Link from "next/link";
77
import { useEffect, useState } from "react";
88
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
99
import { Button } from "../components/ui/button";
10-
import { Sidebar } from "../components/ui/sidebar";
11-
import { navLinks } from "./navLinks";
10+
import { Sidebar, type SidebarLink } from "../components/ui/sidebar";
1211
import { otherLinks } from "./otherLinks";
1312

14-
export function MobileHeader() {
13+
export function MobileHeader(props: {
14+
links: SidebarLink[];
15+
}) {
1516
const [isOpen, setIsOpen] = useState(false);
1617

1718
useEffect(() => {
@@ -27,47 +28,49 @@ export function MobileHeader() {
2728
}, [isOpen]);
2829

2930
return (
30-
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
31-
<header
32-
className="sticky top-0 z-10 flex justify-between gap-4 border-b bg-background px-4 py-4 xl:hidden "
33-
onClick={(e) => {
34-
if (e.target instanceof HTMLAnchorElement) {
35-
setIsOpen(false);
36-
}
37-
}}
38-
>
39-
<Link
40-
className="flex items-center gap-3"
41-
href="/"
42-
aria-label="thirdweb Docs"
43-
title="thirdweb Docs"
44-
>
45-
<Image src={thirdwebIconSrc} className="size-7" alt="" />
46-
<span className="font-bold text-xl leading-none tracking-tight">
47-
Playground
48-
</span>
49-
</Link>
50-
<Button
51-
variant="outline"
52-
className="!h-auto p-2"
53-
onClick={() => {
54-
setIsOpen((v) => !v);
55-
}}
56-
>
57-
{!isOpen ? (
58-
<MenuIcon className="size-6" />
59-
) : (
60-
<XIcon className="size-6" />
61-
)}
62-
</Button>
31+
<>
32+
<header className="sticky top-0 z-10 flex justify-between gap-4 border-b bg-background px-4 py-4 xl:hidden">
33+
<Link
34+
className="flex items-center gap-3"
35+
href="/"
36+
aria-label="thirdweb Docs"
37+
title="thirdweb Docs"
38+
>
39+
<Image src={thirdwebIconSrc} className="size-7" alt="" />
40+
<span className="font-bold text-xl leading-none tracking-tight">
41+
Playground
42+
</span>
43+
</Link>
44+
<Button
45+
variant="outline"
46+
className="!h-auto p-2"
47+
onClick={() => {
48+
setIsOpen((v) => !v);
49+
}}
50+
>
51+
{!isOpen ? (
52+
<MenuIcon className="size-6" />
53+
) : (
54+
<XIcon className="size-6" />
55+
)}
56+
</Button>
57+
</header>
58+
6359
{isOpen && (
64-
<div className="fade-in-0 slide-in-from-top-5 fixed top-[75px] right-0 bottom-0 left-0 z-50 flex animate-in flex-col bg-background duration-200">
60+
<div
61+
className="fade-in-0 slide-in-from-top-5 fixed top-[75px] right-0 bottom-0 left-0 z-50 flex animate-in flex-col bg-background duration-200"
62+
onClickCapture={(e) => {
63+
if (e.target instanceof HTMLElement && e.target.closest("a")) {
64+
setIsOpen(false);
65+
}
66+
}}
67+
>
6568
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
6669
<ScrollShadow
6770
className="grow px-6"
6871
scrollableClassName="max-h-full pt-6"
6972
>
70-
<Sidebar links={navLinks} />
73+
<Sidebar links={props.links} />
7174
</ScrollShadow>
7275
</div>
7376

@@ -87,6 +90,6 @@ export function MobileHeader() {
8790
</div>
8891
</div>
8992
)}
90-
</header>
93+
</>
9194
);
9295
}

apps/playground-web/src/app/globals.css

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
--accent-foreground: 240 5.9% 10%;
2121
--destructive: 0 84.2% 60.2%;
2222
--destructive-foreground: 0 0% 98%;
23+
--destructive-text: 357.15deg 100% 68.72%;
24+
--success-text: 142.09 70.56% 35.29%;
25+
--warning-text: 38 92% 40%;
26+
--inverted-foreground: 0 0% 100%;
27+
--inverted: 0 0% 4%;
2328
--border: 240 5.9% 90%;
2429
--input: 240 5.9% 90%;
2530
--ring: 240 5.9% 10%;
@@ -35,7 +40,7 @@
3540
.dark {
3641
--background: 240deg 2% 11%;
3742
--foreground: 0 0% 98%;
38-
--card: 240deg 2% 11%;
43+
--card: 240deg 2% 13%;
3944
--card-foreground: 0 0% 98%;
4045
--popover: 240deg 2% 11%;
4146
--popover-foreground: 0 0% 98%;
@@ -49,7 +54,13 @@
4954
--accent-foreground: 0 0% 98%;
5055
--destructive: 0 62.8% 30.6%;
5156
--destructive-foreground: 0 0% 98%;
57+
--destructive-text: 360 72% 55%;
58+
--warning-text: 38 92% 50%;
59+
--success-text: 142 75% 50%;
5260
--border: 240deg 2% 20%;
61+
--active-border: 240deg 2% 25%;
62+
--inverted-foreground: 0 0% 0%;
63+
--inverted: 0 0% 100%;
5364
--input: 240deg 2% 20%;
5465
--ring: 240deg 2% 30%;
5566
--chart-1: 220 70% 50%;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import type { ChainMetadata } from "thirdweb/chains";
5+
6+
async function fetchChainsFromApi() {
7+
const res = await fetch("https://api.thirdweb.com/v1/chains");
8+
const json = await res.json();
9+
10+
if (json.error) {
11+
throw new Error(json.error.message);
12+
}
13+
14+
return json.data as ChainMetadata[];
15+
}
16+
17+
export function useAllChainsData() {
18+
const query = useQuery({
19+
queryKey: ["all-chains"],
20+
queryFn: async () => {
21+
const idToChain = new Map<number, ChainMetadata>();
22+
const chains = await fetchChainsFromApi();
23+
24+
for (const c of chains) {
25+
idToChain.set(c.chainId, c);
26+
}
27+
28+
return {
29+
allChains: chains,
30+
idToChain,
31+
};
32+
},
33+
});
34+
35+
return {
36+
isPending: query.isLoading,
37+
data: query.data || { allChains: [], idToChain: new Map() },
38+
};
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useCallback, useState } from "react";
4+
5+
/**
6+
*
7+
* @internal
8+
*/
9+
export function useShowMore<T extends HTMLElement>(
10+
initialItemsToShow: number,
11+
itemsToAdd: number,
12+
) {
13+
// start with showing first `initialItemsToShow` items, when the last item is in view, show `itemsToAdd` more
14+
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
15+
const lastItemRef = useCallback(
16+
(node: T) => {
17+
if (!node) {
18+
return;
19+
}
20+
21+
const observer = new IntersectionObserver(
22+
(entries) => {
23+
if (entries[0]?.isIntersecting) {
24+
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
25+
}
26+
},
27+
{ threshold: 1 },
28+
);
29+
30+
observer.observe(node);
31+
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
32+
},
33+
[itemsToAdd],
34+
);
35+
36+
return { itemsToShow, lastItemRef };
37+
}

0 commit comments

Comments
 (0)