← back to Trademarks Copyright
src/app/admin/subscribers/page.tsx
132 lines
"use client";
import { useEffect, useState } from "react";
type Sub = {
id: number; email: string; name: string | null;
tier: string; status: string; trial_drops_left: number;
signed_up_at: string; last_delivered_at: string | null;
delivered_count: string; open_count: string; ref_count: string;
};
export default function SubscribersPage() {
const [subs, setSubs] = useState<Sub[]>([]);
const [loading, setLoading] = useState(true);
const [q, setQ] = useState("");
const [tierFilter, setTierFilter] = useState("");
async function load() {
setLoading(true);
const r = await fetch("/api/drops/admin/subscribers");
const j = await r.json();
setSubs(j.subscribers || []);
setLoading(false);
}
useEffect(() => { load(); }, []);
async function updateSub(id: number, updates: Record<string, unknown>) {
await fetch(`/api/drops/admin/subscribers/${id}`, {
method: "PATCH", headers: { "content-type": "application/json" },
body: JSON.stringify(updates),
});
load();
}
const filtered = subs.filter((s) => {
if (tierFilter && s.tier !== tierFilter) return false;
if (q) {
const needle = q.toLowerCase();
return s.email.toLowerCase().includes(needle) ||
(s.name && s.name.toLowerCase().includes(needle));
}
return true;
});
const totals = subs.reduce((a, s) => {
a[s.tier] = (a[s.tier] || 0) + 1;
return a;
}, {} as Record<string, number>);
return (
<div>
<h1 className="text-3xl font-semibold">Subscribers</h1>
<p className="text-sm text-ink/60">
{subs.length} total · {totals.trial ?? 0} trial · {totals.standard ?? 0} standard · {totals.pro ?? 0} pro · {totals.comp ?? 0} comp
</p>
<div className="mt-4 flex flex-wrap items-center gap-2">
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="search email / name" className="w-64" />
<select value={tierFilter} onChange={(e) => setTierFilter(e.target.value)}>
<option value="">All tiers</option>
<option value="trial">Trial</option>
<option value="standard">Standard</option>
<option value="pro">Pro</option>
<option value="comp">Comp</option>
</select>
<div className="ml-auto text-sm text-ink/60">{filtered.length} shown</div>
</div>
<div className="card overflow-x-auto mt-3">
<table className="min-w-full text-sm">
<thead className="bg-ink/5 text-left">
<tr>
<th className="p-2">Email</th>
<th className="p-2">Tier</th>
<th className="p-2">Status</th>
<th className="p-2">Trial</th>
<th className="p-2">Sent</th>
<th className="p-2">Opens</th>
<th className="p-2">Refs</th>
<th className="p-2">Signed up</th>
<th className="p-2">Actions</th>
</tr>
</thead>
<tbody>
{loading && <tr><td colSpan={9} className="p-6 text-center text-ink/50">Loading…</td></tr>}
{!loading && filtered.length === 0 && <tr><td colSpan={9} className="p-6 text-center text-ink/50">No matches.</td></tr>}
{filtered.map((s) => {
const delivered = Number(s.delivered_count);
const opens = Number(s.open_count);
const rate = delivered > 0 ? Math.round((opens / delivered) * 100) : 0;
return (
<tr key={s.id} className="border-t border-ink/5 hover:bg-brass/5">
<td className="p-2">
<div className="font-medium">{s.email}</div>
{s.name && <div className="text-xs text-ink/60">{s.name}</div>}
</td>
<td className="p-2">
<select value={s.tier} onChange={(e) => updateSub(s.id, { tier: e.target.value })}>
<option value="trial">trial</option>
<option value="standard">standard</option>
<option value="pro">pro</option>
<option value="comp">comp</option>
</select>
</td>
<td className="p-2">
<select value={s.status} onChange={(e) => updateSub(s.id, { status: e.target.value })}>
<option value="active">active</option>
<option value="paused">paused</option>
<option value="cancelled">cancelled</option>
</select>
</td>
<td className="p-2 text-center">{s.trial_drops_left}</td>
<td className="p-2 text-center">{delivered}</td>
<td className="p-2 text-center">
{opens}
{delivered > 0 && <span className="ml-1 text-xs text-ink/50">({rate}%)</span>}
</td>
<td className="p-2 text-center">{s.ref_count}</td>
<td className="p-2 text-xs text-ink/60">{new Date(s.signed_up_at).toLocaleDateString()}</td>
<td className="p-2 text-xs">
<a href={`/drops/view/${s.id}`} target="_blank" rel="noreferrer" className="text-brass hover:underline">portal →</a>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}