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
75 changes: 75 additions & 0 deletions components/Accounts/AccountsTable.tsx
Original file line number Diff line number Diff line change
@@ -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 <AccountsTableSkeleton />;
}

if (error) {
return (
<p className="text-sm text-red-500">
Failed to load accounts.
</p>
);
}

if (!accounts || accounts.length === 0) {
return (
<p className="text-sm text-muted-foreground">
No accounts with sandboxes found.
</p>
);
}

return (
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-left text-sm">
<thead className="border-b bg-muted/50">
<tr>
<th className="px-4 py-3 font-medium">Account Name</th>
<th className="px-4 py-3 font-medium text-right">Total Sandboxes</th>
<th className="px-4 py-3 font-medium text-right">Last Created</th>
</tr>
</thead>
<tbody className="divide-y">
{accounts.map((account) => (
<tr key={account.account_id} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-3">
<div>
<span className="font-medium">
{account.account_name || "Unnamed"}
</span>
<p className="text-xs text-muted-foreground font-mono">
{account.account_id}
</p>
</div>
</td>
<td className="px-4 py-3 text-right tabular-nums">
{account.total_sandboxes}
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
{formatDate(account.last_created_at)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
31 changes: 31 additions & 0 deletions components/Accounts/AccountsTableSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default function AccountsTableSkeleton() {
return (
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-left text-sm">
<thead className="border-b bg-muted/50">
<tr>
<th className="px-4 py-3 font-medium">Account Name</th>
<th className="px-4 py-3 font-medium text-right">Total Sandboxes</th>
<th className="px-4 py-3 font-medium text-right">Last Created</th>
</tr>
</thead>
<tbody className="divide-y">
{Array.from({ length: 5 }).map((_, i) => (
<tr key={i}>
<td className="px-4 py-3">
<div className="h-4 w-32 animate-pulse rounded bg-muted" />
<div className="mt-1 h-3 w-48 animate-pulse rounded bg-muted" />
</td>
<td className="px-4 py-3 text-right">
<div className="ml-auto h-4 w-8 animate-pulse rounded bg-muted" />
</td>
<td className="px-4 py-3 text-right">
<div className="ml-auto h-4 w-24 animate-pulse rounded bg-muted" />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
15 changes: 10 additions & 5 deletions components/Home/AdminDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import AccountsTable from "@/components/Accounts/AccountsTable";

export default function AdminDashboard() {
return (
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-3xl font-bold tracking-tight">Admin Dashboard</h2>
<p className="text-lg text-muted-foreground">
Welcome back. You have admin access.
</p>
<div className="flex w-full max-w-4xl flex-col gap-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Accounts with Sandboxes</h2>
<p className="text-sm text-muted-foreground">
All accounts that have created sandboxes on the platform.
</p>
</div>
<AccountsTable />
</div>
);
}
6 changes: 5 additions & 1 deletion components/Home/HomeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@ export default function HomeContent() {
);
}

return <AdminDashboard />;
return (
<div className="w-full px-6 py-8">
<AdminDashboard />
</div>
);
}
39 changes: 39 additions & 0 deletions hooks/useAccountsWithSandboxes.ts
Original file line number Diff line number Diff line change
@@ -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<AccountSandboxSummary[]>({
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,
});
}