Skip to content

Commit e8e67de

Browse files
feat: ✨ implement BuildersList component to display checked-in builders
Add a new BuildersList component that fetches and displays a list of builders who have checked in, including their addresses, check-in dates, and graduation status. The component handles loading states and errors, and integrates with the existing contract functions to retrieve necessary data.
1 parent ed2ad64 commit e8e67de

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useState } from "react";
4+
import Link from "next/link";
5+
import type { NextPage } from "next";
6+
import { Address as AddressType } from "viem";
7+
import { usePublicClient } from "wagmi";
8+
import { Address } from "~~/components/scaffold-eth";
9+
import { useScaffoldEventHistory, useScaffoldReadContract } from "~~/hooks/scaffold-eth";
10+
11+
// Helper component to display individual builder details with check-in date
12+
type BuilderDetailsRowProps = {
13+
builderAddress: AddressType;
14+
checkInContractAddress: AddressType;
15+
blockNumber: bigint;
16+
existingProfiles: string[];
17+
};
18+
19+
/**
20+
* Displays a single builder's information including address, contract, and graduation status
21+
*/
22+
const BuilderDetailsRow = ({
23+
builderAddress,
24+
checkInContractAddress,
25+
blockNumber,
26+
existingProfiles,
27+
}: BuilderDetailsRowProps) => {
28+
const publicClient = usePublicClient();
29+
const [checkInDate, setCheckInDate] = useState<string>("Fetching date...");
30+
31+
const { data: graduatedTokenId } = useScaffoldReadContract({
32+
contractName: "BatchRegistry",
33+
functionName: "graduatedTokenId",
34+
args: [builderAddress],
35+
});
36+
37+
const hasGraduated = useMemo(() => graduatedTokenId && Number(graduatedTokenId) > 0, [graduatedTokenId]);
38+
39+
useEffect(() => {
40+
const fetchBlockTimestamp = async () => {
41+
if (!publicClient || blockNumber === undefined) {
42+
setCheckInDate("N/A");
43+
return;
44+
}
45+
try {
46+
const block = await publicClient.getBlock({ blockNumber });
47+
const date = new Date(Number(block.timestamp) * 1000);
48+
setCheckInDate(date.toLocaleDateString());
49+
} catch (error) {
50+
console.error(`Error fetching block timestamp for block ${blockNumber}:`, error);
51+
setCheckInDate("Error");
52+
}
53+
};
54+
fetchBlockTimestamp();
55+
}, [publicClient, blockNumber]);
56+
57+
return (
58+
<div className="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow duration-300 border border-base-300/50 dark:border-base-300/30">
59+
<div className="card-body p-5 md:p-6">
60+
{/* Top section: Builder EOA and Check-in Date */}
61+
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between mb-3">
62+
<div className="mb-2 sm:mb-0">
63+
<Address address={builderAddress} size="lg" />
64+
</div>
65+
<div className="text-xs text-base-content/70 dark:text-base-content/60 mt-1 sm:mt-0">
66+
Checked in: {checkInDate}
67+
</div>
68+
</div>
69+
70+
{/* Divider (for visual separation) */}
71+
<div className="divider my-1"></div>
72+
73+
{/* Details Section */}
74+
<div className="space-y-2 mb-4">
75+
<div className="text-sm">
76+
<span className="font-medium text-base-content/80 dark:text-base-content/70">Contract: </span>
77+
<Address address={checkInContractAddress} size="sm" />
78+
</div>
79+
<div className="text-sm flex items-center gap-2">
80+
<span className="font-medium text-base-content/80 dark:text-base-content/70">Status: </span>
81+
{hasGraduated ? (
82+
<span className="badge badge-sm badge-success text-success-content font-medium">
83+
Graduated (ID: {graduatedTokenId?.toString()})
84+
</span>
85+
) : (
86+
<span className="badge badge-sm badge-neutral text-neutral-content font-medium">Not Graduated</span>
87+
)}
88+
</div>
89+
</div>
90+
91+
{existingProfiles.includes(builderAddress) && (
92+
<div className="card-actions justify-start pt-2">
93+
<Link
94+
href={`/builders/${builderAddress}`}
95+
passHref
96+
className="btn btn-primary btn-sm dark:btn-outline dark:border-primary-content dark:text-primary-content dark:hover:bg-primary-content dark:hover:text-primary dark:hover:border-primary-content"
97+
>
98+
View Profile
99+
</Link>
100+
</div>
101+
)}
102+
</div>
103+
</div>
104+
);
105+
};
106+
107+
const BuildersList: NextPage = () => {
108+
const { data: checkedInCounter } = useScaffoldReadContract({
109+
contractName: "BatchRegistry",
110+
functionName: "checkedInCounter",
111+
});
112+
113+
const {
114+
data: checkedInEvents,
115+
isLoading: isLoadingEvents,
116+
error: errorEvents,
117+
} = useScaffoldEventHistory({
118+
contractName: "BatchRegistry",
119+
eventName: "CheckedIn",
120+
fromBlock: 334314026n, // DEPLOY_BLOCK
121+
watch: true,
122+
});
123+
124+
const buildersWithFirstCheckInBlock = useMemo(() => {
125+
if (!checkedInEvents || checkedInEvents.length === 0) return [];
126+
127+
// Sort events chronologically (earliest block first) to correctly identify each builder's FIRST check-in,
128+
// since a builder may have checked in multiple times with different contracts.
129+
// Handle potential null blockNumbers, though unlikely for valid events.
130+
const sortedEvents = [...checkedInEvents].sort((a, b) => {
131+
const blockA = a.blockNumber ?? 0n; // Default to 0 if null, for robust sorting
132+
const blockB = b.blockNumber ?? 0n; // Default to 0 if null, for robust sorting
133+
if (blockA < blockB) return -1;
134+
if (blockA > blockB) return 1;
135+
return 0;
136+
});
137+
138+
const firstCheckIns = new Map<AddressType, { blockNumber: bigint; checkInContract: AddressType }>();
139+
140+
for (const event of sortedEvents) {
141+
// Skip events with undefined args
142+
if (!event.args) continue;
143+
144+
const builderAddress = event.args.builder as AddressType | undefined;
145+
const contractAddress = event.args.checkInContract as AddressType | undefined;
146+
const blockNumber = event.blockNumber;
147+
148+
// Ensure all necessary data is present and the builder hasn't been added yet.
149+
if (builderAddress && contractAddress && blockNumber !== null && !firstCheckIns.has(builderAddress)) {
150+
firstCheckIns.set(builderAddress, { blockNumber, checkInContract: contractAddress });
151+
}
152+
}
153+
154+
return Array.from(firstCheckIns.entries()).map(([address, data]) => ({
155+
address,
156+
blockNumber: data.blockNumber,
157+
checkInContract: data.checkInContract,
158+
}));
159+
}, [checkedInEvents]);
160+
161+
const [profilesList, setProfilesList] = useState<string[]>([]);
162+
163+
useEffect(() => {
164+
// This approach uses dynamic ES module imports (properly async)
165+
const loadProfiles = async () => {
166+
try {
167+
const profilesModule = await import("~~/generated/existingBuilderProfiles");
168+
setProfilesList(profilesModule.existingBuilderProfiles || []);
169+
} catch {
170+
console.log("No generated profile list found yet. Using empty list for profile links.");
171+
// profilesList remains an empty array
172+
}
173+
};
174+
175+
loadProfiles();
176+
}, []);
177+
178+
return (
179+
<div className="container mx-auto mt-4 px-4 md:px-8 py-8 min-h-screen">
180+
<div className="text-center mb-12">
181+
<h1 className="text-4xl font-bold text-primary mb-2 dark:text-primary-content">Batch 16 Builders</h1>
182+
<p className="text-xl text-base-content/80 dark:text-base-content/70">Checked-in Members</p>
183+
<p className="text-lg text-base-content/70 dark:text-base-content/60 mt-2">
184+
Total Checked In: {checkedInCounter === undefined ? "..." : (checkedInCounter?.toString() ?? "0")}
185+
</p>
186+
</div>
187+
188+
<div className="max-w-3xl mx-auto">
189+
{isLoadingEvents && (
190+
<div className="text-center py-10">
191+
<span className="loading loading-lg loading-spinner text-primary"></span>
192+
<p className="mt-4 text-lg text-base-content/70 dark:text-base-content/60">
193+
Loading checked-in builders...
194+
</p>
195+
</div>
196+
)}
197+
{errorEvents && (
198+
<div role="alert" className="alert alert-error shadow-md">
199+
<span className="text-error-content">Error loading events. (Message: {errorEvents.message})</span>
200+
</div>
201+
)}
202+
{!isLoadingEvents && !errorEvents && (
203+
<div className="grid grid-cols-1 gap-6">
204+
{buildersWithFirstCheckInBlock.length === 0 ? (
205+
<div className="card bg-base-100 shadow-md border border-base-300/50 dark:border-base-300/30">
206+
<div className="card-body items-center text-center p-6 md:p-8">
207+
<p className="text-lg text-base-content/70 dark:text-base-content/60 py-8">
208+
No builders have checked in yet.
209+
</p>
210+
</div>
211+
</div>
212+
) : (
213+
buildersWithFirstCheckInBlock.map(({ address, blockNumber, checkInContract }) => (
214+
<BuilderDetailsRow
215+
key={address}
216+
builderAddress={address}
217+
checkInContractAddress={checkInContract}
218+
blockNumber={blockNumber}
219+
existingProfiles={profilesList}
220+
/>
221+
))
222+
)}
223+
</div>
224+
)}
225+
</div>
226+
</div>
227+
);
228+
};
229+
230+
export default BuildersList;

0 commit comments

Comments
 (0)