← 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>
  );
}