feat: add Waitlist admin page
Some checks failed
build-and-release / build (push) Failing after 7s

Lists nxtgauge.com coming-soon signups (email + date, paginated 200/
page) via GET /api/admin/waitlist, with a CSV export button. Added to
the sidebar nav under the Pricing/Credit/AI/Coupon/Discount group.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-08-14 04:07:18 +05:30
parent b9c209aa67
commit 26667c7ede
2 changed files with 168 additions and 0 deletions

View file

@ -268,6 +268,12 @@ const GROUPS: NavItem[][] = [
icon: Percent,
moduleKeys: ["DISCOUNT_MANAGEMENT", "DISCOUNTS"],
},
{
href: "/admin/waitlist",
label: "Waitlist",
icon: Mail,
moduleKeys: ["WAITLIST_MANAGEMENT", "WAITLIST"],
},
{
href: "/admin/tax",
label: "Tax Management",

View file

@ -0,0 +1,162 @@
import { createSignal, createResource, Show, For } from "solid-js";
type WaitlistSignup = {
email: string;
created_at: string;
};
type ListWaitlistResponse = {
signups: WaitlistSignup[];
total: number;
page: number;
limit: number;
};
const API = "";
function authHeaders(): Record<string, string> {
const token =
typeof sessionStorage !== "undefined"
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
: "";
return token ? { Authorization: `Bearer ${token}` } : {};
}
async function fetchWaitlist(page: number): Promise<ListWaitlistResponse> {
const res = await fetch(`${API}/api/admin/waitlist?page=${page}&limit=200`, {
headers: authHeaders(),
credentials: "include",
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
return res.json();
}
function toCsv(rows: WaitlistSignup[]): string {
const escape = (v: string) => `"${v.replace(/"/g, '""')}"`;
const header = "email,signed_up_at\n";
const body = rows.map((r) => `${escape(r.email)},${escape(r.created_at)}`).join("\n");
return header + body;
}
function downloadCsv(rows: WaitlistSignup[]) {
const csv = toCsv(rows);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `nxtgauge-waitlist-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
export default function WaitlistPage() {
const [page, setPage] = createSignal(1);
const [data, { refetch }] = createResource(page, fetchWaitlist);
const totalPages = () => {
const d = data();
return d ? Math.max(1, Math.ceil(d.total / d.limit)) : 1;
};
return (
<div style="padding:24px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<div>
<h1 style="margin:0;font-size:22px;font-weight:700;color:#111827">Waitlist</h1>
<p style="margin:4px 0 0;font-size:13px;color:#6B7280">
Emails captured from the nxtgauge.com "Notify Me" coming-soon form.
</p>
</div>
<div style="display:flex;gap:8px">
<button
type="button"
style="display:inline-flex;height:36px;align-items:center;gap:6px;border-radius:8px;border:1px solid #D1D5DB;background:#fff;padding:0 14px;font-size:13px;font-weight:600;color:#0f172a;cursor:pointer"
onClick={() => refetch()}
>
Refresh
</button>
<button
type="button"
disabled={!data() || data()!.signups.length === 0}
style="display:inline-flex;height:36px;align-items:center;gap:6px;border-radius:8px;border:none;background:#FF5E13;padding:0 14px;font-size:13px;font-weight:600;color:#fff;cursor:pointer;opacity:1"
onClick={() => data() && downloadCsv(data()!.signups)}
>
Export CSV
</button>
</div>
</div>
<Show when={data.loading}>
<div style="text-align:center;padding:48px;color:#64748b">Loading...</div>
</Show>
<Show when={data.error}>
<div style="border:1px solid #FECACA;background:#FEF2F2;color:#B91C1C;padding:12px 16px;border-radius:8px;margin-bottom:16px;font-size:13px">
{String(data.error?.message || data.error)}
</div>
</Show>
<Show when={data() && !data.loading}>
<p style="font-size:13px;color:#6B7280;margin-bottom:10px">
{data()!.total} total signup{data()!.total === 1 ? "" : "s"}
</p>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Email</th>
<th>Signed up</th>
</tr>
</thead>
<tbody>
<Show when={data()!.signups.length === 0}>
<tr>
<td colspan="2" style="text-align:center;padding:32px;color:#94a3b8">
No signups yet.
</td>
</tr>
</Show>
<For each={data()!.signups}>
{(row) => (
<tr class="hover:bg-slate-50">
<td class="font-medium text-slate-900">{row.email}</td>
<td class="text-slate-600">{new Date(row.created_at).toLocaleString()}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
<Show when={totalPages() > 1}>
<div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-top:16px">
<button
type="button"
disabled={page() <= 1}
style="padding:6px 12px;border-radius:6px;border:1px solid #D1D5DB;background:#fff;font-size:13px;cursor:pointer"
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
Previous
</button>
<span style="font-size:13px;color:#4B5563">
Page {page()} of {totalPages()}
</span>
<button
type="button"
disabled={page() >= totalPages()}
style="padding:6px 12px;border-radius:6px;border:1px solid #D1D5DB;background:#fff;font-size:13px;cursor:pointer"
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
>
Next
</button>
</div>
</Show>
</Show>
</div>
);
}