Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,8 @@
},
"engines": {
"node": ">=20.18.3"
},
"dependencies": {
"graphql-request": "^7.3.5"
}
}
34 changes: 34 additions & 0 deletions packages/nextjs/app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { QueryClient, QueryClientProvider, isServer } from "@tanstack/react-query";

// /app/QueryClientProvider.tsx
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
const queryClient = getQueryClient();
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
34 changes: 34 additions & 0 deletions packages/nextjs/app/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { QueryClient, QueryClientProvider, isServer } from "@tanstack/react-query";

// /app/QueryClientProvider.tsx
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
const queryClient = getQueryClient();
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
15 changes: 15 additions & 0 deletions packages/nextjs/app/builders/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import BuildersList from "~~/components/BuildersList";
import { getProcessedBuilders } from "~~/utils/builders";

const BuildersPage = async () => {
const processedBuilders = await getProcessedBuilders();

return (
<div className="flex items-center flex-col grow pt-10">
<h1 className="text-2xl font-bold mb-4">Builders List</h1>
<BuildersList builders={processedBuilders} />
</div>
);
};

export default BuildersPage;
9 changes: 6 additions & 3 deletions packages/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Providers from "./QueryClientProvider";
import "@rainbow-me/rainbowkit/styles.css";
import "@scaffold-ui/components/styles.css";
import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
Expand All @@ -14,9 +15,11 @@ const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
<html suppressHydrationWarning>
<body>
<ThemeProvider enableSystem>
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
</ThemeProvider>
<Providers>
<ThemeProvider enableSystem>
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
</ThemeProvider>
</Providers>
</body>
</html>
);
Expand Down
38 changes: 38 additions & 0 deletions packages/nextjs/components/BuildersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import Link from "next/link";
import { Address } from "@scaffold-ui/components";
import { ProcessedBuilder } from "~~/utils/builders";

type BuildersListProps = {
builders: ProcessedBuilder[];
};

const BuildersList = ({ builders }: BuildersListProps) => {
return (
<div className="w-full max-w-4xl px-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{builders.map(builder => (
<div
key={`${builder.builder}`}
className="bg-base-100 border border-base-300 rounded-lg p-4 hover:shadow-[0_0_20px_rgba(255,255,255,0.2)] flex flex-col gap-2"
>
<Address
address={builder.builder as `0x${string}`}
format="short"
onlyEnsOrAddress={true}
disableAddressLink
/>
{builder.hasPage && builder.checksummedAddress && (
<Link href={`/builders/${builder.checksummedAddress}`} className="btn btn-primary btn-sm mt-2">
View Profile
</Link>
)}
</div>
))}
</div>
</div>
);
};

export default BuildersList;
4 changes: 4 additions & 0 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const menuLinks: HeaderMenuLink[] = [
href: "/debug",
icon: <BugAntIcon className="h-4 w-4" />,
},
{
label: "Builders",
href: "/builders",
},
];

export const HeaderMenuLinks = () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"blo": "^1.2.0",
"burner-connector": "0.0.18",
"daisyui": "^5.0.9",
"graphql": "^16.12.0",
"graphql-request": "^7.3.5",
"kubo-rpc-client": "^5.0.2",
"next": "^15.2.3",
"next-nprogress-bar": "^2.3.13",
Expand Down
60 changes: 60 additions & 0 deletions packages/nextjs/utils/builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from "fs";
import { gql, request } from "graphql-request";
import path from "path";

const GRAPHQL_QUERY = gql`
{
checkedIns {
builder
}
}
`;

const GRAPHQL_URL = "https://api.studio.thegraph.com/query/1717360/batch-22-buidlguidl/version/latest";

export type ProcessedBuilder = {
builder: string;
index: number;
hasPage: boolean;
checksummedAddress: string | null;
};

/**
* Fetches builders data from GraphQL, removes duplicates, reads builder pages from filesystem,
* and enriches each builder with page information.
* @returns Array of processed builders with page information
*/
export async function getProcessedBuilders(): Promise<ProcessedBuilder[]> {
// Fetch data from GraphQL
const data = await request(GRAPHQL_URL, GRAPHQL_QUERY, {});
const checkedIns = (data?.checkedIns || []) as Array<{ builder: string }>;

// Remove duplicates
const uniqueBuilders = Array.from(
new Map(checkedIns.map((item, index) => [item.builder, { ...item, index }])).values(),
) as Array<{ builder: string; index: number }>;

// Read folders from filesystem
const buildersDir = path.join(process.cwd(), "app/builders");
const folders = fs
.readdirSync(buildersDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);

// Create folder map (lowercase -> checksummed address)
const folderMap = new Map<string, string>();
folders.forEach(folder => {
folderMap.set(folder.toLowerCase(), folder);
});

// Enrich builders with page information
return uniqueBuilders.map(builder => {
const checksummedAddress = folderMap.get(builder.builder.toLowerCase());
return {
builder: builder.builder,
index: builder.index,
hasPage: !!checksummedAddress,
checksummedAddress: checksummedAddress || null,
};
});
}
30 changes: 30 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,15 @@ __metadata:
languageName: node
linkType: hard

"@graphql-typed-document-node/core@npm:^3.2.0":
version: 3.2.0
resolution: "@graphql-typed-document-node/core@npm:3.2.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d
languageName: node
linkType: hard

"@heroicons/react@npm:^2.1.5":
version: 2.1.5
resolution: "@heroicons/react@npm:2.1.5"
Expand Down Expand Up @@ -4056,6 +4065,8 @@ __metadata:
eslint-config-next: ^15.2.3
eslint-config-prettier: ^10.1.1
eslint-plugin-prettier: ^5.2.4
graphql: ^16.12.0
graphql-request: ^7.3.5
kubo-rpc-client: ^5.0.2
next: ^15.2.3
next-nprogress-bar: ^2.3.13
Expand Down Expand Up @@ -10931,6 +10942,24 @@ __metadata:
languageName: node
linkType: hard

"graphql-request@npm:^7.3.5":
version: 7.3.5
resolution: "graphql-request@npm:7.3.5"
dependencies:
"@graphql-typed-document-node/core": ^3.2.0
peerDependencies:
graphql: 14 - 16
checksum: 05d6ee3c84f197bc14669d805ec942f4ed8230e091b8c52d2cebdaebbacf19760d830a6d03c5fe2d739b74a72d742eef9fef246718dbab95bccbb5ed8a5919a2
languageName: node
linkType: hard

"graphql@npm:^16.12.0":
version: 16.12.0
resolution: "graphql@npm:16.12.0"
checksum: c0d2435425270c575091861c9fd82d7cebc1fb1bd5461e05c36521a988f69c5074461e27b89ab70851fabc72ec9d988235f288ba7bbeff67d08a973e8b9d6d3d
languageName: node
linkType: hard

"h3@npm:^1.15.2":
version: 1.15.3
resolution: "h3@npm:1.15.3"
Expand Down Expand Up @@ -16398,6 +16427,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "se-2@workspace:."
dependencies:
graphql-request: ^7.3.5
husky: ^9.1.6
lint-staged: ^15.2.10
languageName: unknown
Expand Down