diff --git a/components/Accounts/AccountsTable.tsx b/components/Accounts/AccountsTable.tsx
new file mode 100644
index 0000000..9ba4bf6
--- /dev/null
+++ b/components/Accounts/AccountsTable.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { useAccountsWithSandboxes } from "@/hooks/useAccountsWithSandboxes";
+import AccountsTableSkeleton from "./AccountsTableSkeleton";
+
+/**
+ * Formats a timestamp string to a readable date/time.
+ */
+function formatDate(dateStr: string | null): string {
+ if (!dateStr) return "—";
+ return new Date(dateStr).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+}
+
+export default function AccountsTable() {
+ const { data: accounts, isLoading, error } = useAccountsWithSandboxes();
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+ Failed to load accounts.
+
+ );
+ }
+
+ if (!accounts || accounts.length === 0) {
+ return (
+
+ No accounts with sandboxes found.
+
+ );
+ }
+
+ return (
+
+
+
+
+ | Account Name |
+ Total Sandboxes |
+ Last Created |
+
+
+
+ {accounts.map((account) => (
+
+
+
+
+ {account.account_name || "Unnamed"}
+
+
+ {account.account_id}
+
+
+ |
+
+ {account.total_sandboxes}
+ |
+
+ {formatDate(account.last_created_at)}
+ |
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/Accounts/AccountsTableSkeleton.tsx b/components/Accounts/AccountsTableSkeleton.tsx
new file mode 100644
index 0000000..5ce7c9d
--- /dev/null
+++ b/components/Accounts/AccountsTableSkeleton.tsx
@@ -0,0 +1,31 @@
+export default function AccountsTableSkeleton() {
+ return (
+
+
+
+
+ | Account Name |
+ Total Sandboxes |
+ Last Created |
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/Home/AdminDashboard.tsx b/components/Home/AdminDashboard.tsx
index d6c29a1..6fa679e 100644
--- a/components/Home/AdminDashboard.tsx
+++ b/components/Home/AdminDashboard.tsx
@@ -1,10 +1,15 @@
+import AccountsTable from "@/components/Accounts/AccountsTable";
+
export default function AdminDashboard() {
return (
-
-
Admin Dashboard
-
- Welcome back. You have admin access.
-
+
+
+
Accounts with Sandboxes
+
+ All accounts that have created sandboxes on the platform.
+
+
+
);
}
diff --git a/components/Home/HomeContent.tsx b/components/Home/HomeContent.tsx
index 39d7882..dd21e4e 100644
--- a/components/Home/HomeContent.tsx
+++ b/components/Home/HomeContent.tsx
@@ -35,5 +35,9 @@ export default function HomeContent() {
);
}
- return
;
+ return (
+
+ );
}
diff --git a/hooks/useAccountsWithSandboxes.ts b/hooks/useAccountsWithSandboxes.ts
new file mode 100644
index 0000000..7f18071
--- /dev/null
+++ b/hooks/useAccountsWithSandboxes.ts
@@ -0,0 +1,39 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { usePrivy } from "@privy-io/react-auth";
+import { API_BASE_URL } from "@/lib/consts";
+
+export interface AccountSandboxSummary {
+ account_id: string;
+ account_name: string | null;
+ total_sandboxes: number;
+ last_created_at: string | null;
+}
+
+/**
+ * Fetches all accounts with sandbox summaries from the admin endpoint.
+ * Requires admin authentication.
+ */
+export function useAccountsWithSandboxes() {
+ const { ready, authenticated, getAccessToken } = usePrivy();
+
+ return useQuery
({
+ queryKey: ["accountsWithSandboxes"],
+ queryFn: async () => {
+ const token = await getAccessToken();
+ if (!token) return [];
+
+ const res = await fetch(
+ `${API_BASE_URL}/api/admins/accounts-with-sandboxes`,
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (!res.ok) return [];
+
+ const data = await res.json();
+ return data.accounts ?? [];
+ },
+ enabled: ready && authenticated,
+ });
+}