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:
parent
b9c209aa67
commit
26667c7ede
2 changed files with 168 additions and 0 deletions
|
|
@ -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",
|
||||
|
|
|
|||
162
src/routes/admin/waitlist.tsx
Normal file
162
src/routes/admin/waitlist.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue