"use client"; import { useEffect, useState } from "react"; import { Button, Field, Input } from "@/components/ui/form"; type AdminBetaRequestDto = { id: number; uuid: string; email: string; displayName?: string | null; invited: boolean; verified: boolean; active: boolean; createdAt?: string; updatedAt?: string; }; type PageResponse = { content: T[]; number: number; size: number; totalElements: number; totalPages: number; }; // Expected invite response shape (adjust fields if your DTO differs) type AdminInviteResponse = { ok?: boolean; email?: string; // backend returns this inviteUrl?: string; // keep these optional if you ever add them later magicUrl?: string; token?: string; }; async function fetchJson(path: string, init: RequestInit = {}) { const res = await fetch(path, { ...init, headers: { "Content-Type": "application/json", ...(init.headers ?? {}), }, cache: "no-store", }); const text = await res.text().catch(() => ""); const data = text ? (() => { try { return JSON.parse(text); } catch { return text; } })() : null; if (!res.ok) { const msg = typeof data === "string" ? data : data?.message || data?.error || `Request failed (${res.status})`; throw new Error(msg); } return data; } function Badge({ ok, yes = "Yes", no = "No", }: { ok: boolean; yes?: string; no?: string; }) { return ( {ok ? yes : no} ); } export default function AdminBetaInvitesPage() { // ------------------------------ // Batch invites // ------------------------------ const [dryRun, setDryRun] = useState(true); const [limit, setLimit] = useState("25"); const [tokenMinutes, setTokenMinutes] = useState("30"); const [loadingBatch, setLoadingBatch] = useState(false); const [batchResult, setBatchResult] = useState(null); const [batchError, setBatchError] = useState(null); async function runInvites() { setLoadingBatch(true); setBatchError(null); setBatchResult(null); try { const qs = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit || "0"), tokenMinutes: String(tokenMinutes || "30"), }); const data = await fetchJson(`/api/admin/beta-invites?${qs.toString()}`, { method: "POST", }); setBatchResult(data); await loadRequests(); } catch (e: any) { setBatchError(e?.message ?? "Failed"); } finally { setLoadingBatch(false); } } // ------------------------------ // Requests table // ------------------------------ const [page, setPage] = useState(0); const size = 25; const [loadingList, setLoadingList] = useState(false); const [listError, setListError] = useState(null); const [requests, setRequests] = useState | null>(null); // per-row loading state const [rowBusy, setRowBusy] = useState>({}); // last invite response (for visibility / debug) const [lastInvite, setLastInvite] = useState( null ); async function loadRequests() { setLoadingList(true); setListError(null); try { const data = (await fetchJson( `/api/admin/beta/requests?page=${page}&size=${size}`, { method: "GET" } )) as PageResponse; setRequests(data); } catch (e: any) { setListError(e?.message ?? "Failed to load beta requests"); } finally { setLoadingList(false); } } async function inviteSingle(userId: number): Promise { // calls backend: POST /api/v1/admin/beta/requests/{userId}/invite return (await fetchJson(`/api/admin/beta/requests/${userId}/invite`, { method: "POST", })) as AdminInviteResponse; } async function onSendInviteEmail(userId: number) { setListError(null); setLastInvite(null); setRowBusy((m) => ({ ...m, [userId]: true })); try { const resp = await inviteSingle(userId); setLastInvite(resp); await loadRequests(); } catch (e: any) { setListError(e?.message ?? "Failed to send invite"); } finally { setRowBusy((m) => ({ ...m, [userId]: false })); } } async function onCopyInviteLink(userId: number) { setListError(null); setLastInvite(null); setRowBusy((m) => ({ ...m, [userId]: true })); try { const resp = await inviteSingle(userId); setLastInvite(resp); await loadRequests(); // build URL const url = resp.inviteUrl || // what your backend returns resp.magicUrl || (resp.token ? `${window.location.origin}/beta/magic?token=${resp.token}` : null); if (!url) { throw new Error( `Invite response missing inviteUrl. Backend should return {"inviteUrl": "..."}` ); } await navigator.clipboard.writeText(url); } catch (e: any) { setListError(e?.message ?? "Failed to copy invite link"); } finally { setRowBusy((m) => ({ ...m, [userId]: false })); } } useEffect(() => { loadRequests(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page]); return (

Beta Access

Review pending beta requests and send invite links (batch or per-user).

{/* Left: Batch Invites */}

Batch Invites

Runs the backend batch invite sender.

{dryRun ? "Dry run" : "Live send"}
setLimit(e.target.value)} /> setTokenMinutes(e.target.value)} />
{batchError && (
{batchError}
)} {batchResult && (
              {JSON.stringify(batchResult, null, 2)}
            
)}
{/* Right: Requests List */}

Beta Requests

Users with role BETA and{" "} is_active=false.

{listError && (
{listError}
)}
{" "} {requests?.content?.length ? ( requests.content.map((u) => { const busy = !!rowBusy[u.id]; return ( ); }) ) : ( )}
Email Invited Verified Active Actions
{u.email}
{u.displayName ?? "—"} • #{u.id}
{loadingList ? "Loading…" : "No pending beta requests."}
{/* pager */}
Page {requests ? requests.number + 1 : 1} of{" "} {requests ? requests.totalPages : 1} •{" "} {requests ? requests.totalElements : 0} total
{/* optional debug panel */} {lastInvite && (
              {JSON.stringify(lastInvite, null, 2)}
            
)}
); }