From 32d227e33dfeac04ca852431b99e9285e8d9a7f5 Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Tue, 27 Jan 2026 14:42:03 +0000 Subject: [PATCH] Surface some commitment data Same changes as: https://app.graphite.com/github/pr/sunrisedotdev/sonar-example-react/5/Surface-some-commitment-data --- package.json | 4 +- pnpm-lock.yaml | 22 ++-- src/app/Provider.tsx | 15 ++- .../components/sale/CommitmentDataCard.tsx | 119 ++++++++++++++++++ src/app/page.tsx | 36 ++++-- 5 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 src/app/components/sale/CommitmentDataCard.tsx diff --git a/package.json b/package.json index 36a1330..1a61515 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "format": "prettier --write ." }, "dependencies": { - "@echoxyz/sonar-core": "^0.13.0", - "@echoxyz/sonar-react": "^0.12.2", + "@echoxyz/sonar-core": "^0.14.0", + "@echoxyz/sonar-react": "^0.13.0", "@tanstack/react-query": "^5.90.12", "connectkit": "^1.9.1", "next": "^15.5.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ffacc0..946d10a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,11 +12,11 @@ importers: .: dependencies: '@echoxyz/sonar-core': - specifier: ^0.13.0 - version: 0.13.0 + specifier: ^0.14.0 + version: 0.14.0 '@echoxyz/sonar-react': - specifier: ^0.12.2 - version: 0.12.2(react@18.3.1) + specifier: ^0.13.0 + version: 0.13.0(react@18.3.1) '@tanstack/react-query': specifier: ^5.90.12 version: 5.90.16(react@18.3.1) @@ -283,11 +283,11 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@echoxyz/sonar-core@0.13.0': - resolution: {integrity: sha512-JKNzkTxrihiwwnvLrF/Oe3GXkKxRwMlPTStbs41gZg7eWBJ0EfgzSLJMFNNjO+PRbgvTbStavoLvQ7BHx1ljuQ==} + '@echoxyz/sonar-core@0.14.0': + resolution: {integrity: sha512-uzUTIozGdS4hBV0RnjT5mXi8IbNrvxY97/gs79PN32dGcKyZryzbMIvgKwO2vzB9YbA170sUEksA2iTB64FBLQ==} - '@echoxyz/sonar-react@0.12.2': - resolution: {integrity: sha512-iuQQrAFiNMsH+NRvaSQvGFsAiYJRQHTDYO774uENkJGsZPtP39JYRhqXw3PCY0cr4CzuCURu0fqaQx7aAl1Xrg==} + '@echoxyz/sonar-react@0.13.0': + resolution: {integrity: sha512-pUHXT2nbzKLoWIWRVirq0dD3HYoBcsq5028EyL+vFx+O9Q4p3sfUU3bVGfuC7P1KygrRKnijQjOS3hnDTL3jVQ==} peerDependencies: react: '>=18' @@ -5126,11 +5126,11 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@echoxyz/sonar-core@0.13.0': {} + '@echoxyz/sonar-core@0.14.0': {} - '@echoxyz/sonar-react@0.12.2(react@18.3.1)': + '@echoxyz/sonar-react@0.13.0(react@18.3.1)': dependencies: - '@echoxyz/sonar-core': 0.13.0 + '@echoxyz/sonar-core': 0.14.0 react: 18.3.1 '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': diff --git a/src/app/Provider.tsx b/src/app/Provider.tsx index 6cffa44..f827a02 100644 --- a/src/app/Provider.tsx +++ b/src/app/Provider.tsx @@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; import { baseSepolia } from "wagmi/chains"; import { SessionProvider } from "./hooks/use-session"; +import { sonarConfig } from "@/lib/config"; +import { SonarProvider } from "@echoxyz/sonar-react"; const config = createConfig( getDefaultConfig({ @@ -27,11 +29,14 @@ const queryClient = new QueryClient(); export const Provider = ({ children }: { children: React.ReactNode }) => { return ( - - - {children} - - + {/* Only required for un-authenticated requests direct from the frontend (e.g. to read sale commitment data) */} + + + + {children} + + + ); }; diff --git a/src/app/components/sale/CommitmentDataCard.tsx b/src/app/components/sale/CommitmentDataCard.tsx new file mode 100644 index 0000000..cb44b70 --- /dev/null +++ b/src/app/components/sale/CommitmentDataCard.tsx @@ -0,0 +1,119 @@ +import { useCommitmentData } from "@echoxyz/sonar-react"; +import { Commitment, WalletTokenAmount } from "@echoxyz/sonar-core"; + +function formatAmount(amount: bigint, decimals: number): string { + const divisor = BigInt(10 ** decimals); + const integerPart = amount / divisor; + return `$${integerPart.toLocaleString()}`; +} + +function calculateCommitmentTotal(amounts: WalletTokenAmount[]): bigint { + return amounts.reduce((sum, item) => sum + BigInt(item.Amount), BigInt(0)); +} + +// The timestamp is a string in ISO 8601 format +function formatTimestamp(timestamp: string ): string { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +interface CommitmentRowProps { + commitment: Commitment; + decimals: number; +} + +function CommitmentRow({ commitment, decimals }: CommitmentRowProps) { + // The commitment data gives the amount by wallet and payment token, + // but for simplicity, we just show the total amount and the wallet with the most committed amount. + const total = calculateCommitmentTotal(commitment.Amounts); + + return ( +
+
+ + {formatAmount(total, decimals)} + + {commitment.SaleSpecificEntityID} +
+ {formatTimestamp(commitment.CreatedAt)} +
+ ); +} + +export function CommitmentDataCard({ saleUUID }: { saleUUID: string }) { + // Note: this is using a public API (no authentication required), + // so it doesn't matter whether we call this from the frontend or the backend. + const { loading, commitmentData, error } = useCommitmentData({ saleUUID, pollingIntervalMs: 10000 }); + + // Only show loading placeholder when there's no existing data to display + if (loading && !commitmentData) { + return ( +
+

Loading commitment data...

+
+ ); + } + + if (error && !commitmentData) { + return ( +
+

Error loading commitment data

+

{error.message}

+
+ ); + } + + if (!commitmentData) { + return null; + } + + // Commitments are sorted by CreatedAt in descending order, so we can just slice the first 20 + const recentCommitments = commitmentData.Commitments.slice(0, 20); + + return ( +
+ {/* Summary Stats */} +
+
+

Total Committers

+

+ {commitmentData.UniqueCommitmentCount.toLocaleString()} +

+
+
+

Total Committed

+

+ {formatAmount(BigInt(commitmentData.TotalCommitmentAmount), commitmentData.PaymentTokenDecimals)} +

+
+
+ + {/* Recent Commitments */} + {recentCommitments.length > 0 && ( +
+

Recent Commitments

+
+ {recentCommitments.map((commitment) => ( + + ))} +
+
+ )} + + {recentCommitments.length === 0 && ( +
+

No commitments yet

+
+ )} +
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 41f392a..a512478 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,6 +14,7 @@ import CommitCard from "./components/sale/CommitCard"; import { EntitiesList } from "./components/registration/EntitiesList"; import { EligibilityResults } from "./components/registration/EligibilityResults"; import { ConnectKitButton } from "connectkit"; +import { CommitmentDataCard } from "./components/sale/CommitmentDataCard"; export default function Home() { const [saleIsLive, setSaleIsLive] = useState(false); @@ -189,24 +190,33 @@ export default function Home() { )} {/* Sale Phase */} - {sonarConnected && saleIsLive && ( + {saleIsLive && (
-

Your Entity Information

- - - - {isEligible && address && ( + {sonarConnected && (
-

Commit funds

- +

Your Entity Information

+ + + + {isEligible && address && ( +
+

Commit funds

+ +
+ )} + + {entity && !isEligible && }
)} - {entity && !isEligible && } +
+

Sale Commitment Data

+ +
)}