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 && (
+
+ )}
+
+ );
+}
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
+
+
)}