"use client"; import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import AuthLayout from "../ui/AuthLayout"; import { clearToken, getApiBase, setToken } from "../lib/auth"; type InviteInfo = { code: string; email_bound?: boolean; label?: string | null; description?: string | null; enabled: boolean; is_expired?: boolean; is_usable?: boolean; expires_at?: string | null; max_uses?: number | null; use_count?: number | null; remaining_uses?: number | null; profile?: { id: number; name: string; description?: string | null; } | null; }; const formatDate = (value?: string | null) => { if (!value) return "Never"; const date = new Date(value); if (Number.isNaN(date.valueOf())) return value; return date.toLocaleString(); }; function SignupPageContent() { const router = useRouter(); const searchParams = useSearchParams(); const [inviteCode, setInviteCode] = useState(searchParams.get("code") ?? ""); const [invite, setInvite] = useState(null); const [inviteLoading, setInviteLoading] = useState(false); const [loading, setLoading] = useState(false); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(null); const [status, setStatus] = useState(null); const canSubmit = useMemo(() => { return Boolean( invite?.is_usable && (invite.email_bound || email.trim()) && username.trim() && password && !loading && !inviteLoading, ); }, [invite, email, username, password, loading, inviteLoading]); const lookupInvite = useCallback(async (code: string) => { const trimmed = code.trim(); if (!trimmed) { setInvite(null); return; } setInviteLoading(true); setError(null); setStatus(null); try { const baseUrl = getApiBase(); const response = await fetch(`${baseUrl}/auth/invites/${encodeURIComponent(trimmed)}`); if (!response.ok) { const text = await response.text(); throw new Error(text || "Invite not found"); } const data = await response.json(); setInvite(data?.invite ?? null); setStatus("Invite loaded."); } catch (err) { console.error(err); setInvite(null); setError("Invite code not found or unavailable."); } finally { setInviteLoading(false); } }, []); useEffect(() => { const initialCode = searchParams.get("code") ?? ""; if (initialCode) { setInviteCode(initialCode); void lookupInvite(initialCode); } }, [lookupInvite, searchParams]); const submit = async (event: React.FormEvent) => { event.preventDefault(); if (password !== confirmPassword) { setError("Passwords do not match."); return; } if (!inviteCode.trim()) { setError("Invite code is required."); return; } if (!invite?.is_usable) { setError("Invite is not usable. Refresh invite details or ask an admin for a new code."); return; } setLoading(true); setError(null); setStatus(null); try { clearToken(); const baseUrl = getApiBase(); const response = await fetch(`${baseUrl}/auth/signup`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ invite_code: inviteCode, username: username.trim(), ...(!invite.email_bound ? { email: email.trim() } : {}), password, }), }); if (!response.ok) { const text = await response.text(); throw new Error(text || "Sign-up failed"); } const data = await response.json(); if (data?.authenticated) { setToken("cookie"); window.location.href = "/welcome"; return; } throw new Error("Sign-up did not complete"); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : "Unable to create account."); } finally { setLoading(false); } }; return (
{invite && (
{invite.label || invite.code} {invite.is_usable ? "Ready" : "Unavailable"}
{invite.description &&

{invite.description}

}
Invite details
Code: {invite.code} Expires: {formatDate(invite.expires_at)} Remaining uses: {invite.remaining_uses ?? "Unlimited"} Profile: {invite.profile?.name || "None"}
)} {invite?.email_bound ? (

Your account will use the email address this invitation was sent to. This invitation can be used once.

) : ( )} {error && (
{error}
)} {status && (
{status}
)}
); } export default function SignupPage() { return (

Loading sign-up…

} >
); }