"use client"; /** * DiscipleLinkModal — links a platform account to one of Śrīla Prabhupāda's * initiated disciples (or clears an existing link). * * The disciple directory lives in Mongo and the link column on the Postgres * users row, so the picker searches server-side via /admin/disciples. Each * option reports whether another account already claims it, letting us disable * those rows up front instead of surfacing a 409 after the click. * * Clicking a free row links immediately — the action is one field, instantly * reversible from the same modal, so a confirm step would only add friction. */ import { useCallback, useEffect, useState } from "react"; import { Check, Link2Off, Search } from "lucide-react"; import { toast } from "sonner"; import { api, AdminError, type AdminDisciple, type AdminUser } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/modal"; import { cn } from "@/lib/utils"; import { Clamp } from "@/components/ui/clamp"; /** "Vṛndāvana · 1972" — whichever halves we know. */ function placeYear(d: AdminDisciple): string | null { const year = d.initiation_date ? new Date(d.initiation_date).getFullYear() : null; const parts = [d.initiation_place, Number.isFinite(year) ? year : null].filter( Boolean, ); return parts.length ? parts.join(" · ") : null; } export function DiscipleLinkModal({ user, onClose, onSaved, }: { user: AdminUser | null; onClose: () => void; onSaved: () => void; }) { const [q, setQ] = useState(""); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [busyId, setBusyId] = useState(null); // Reset the query each time the modal opens for a different user, so a // stale search doesn't carry over between rows. useEffect(() => { if (user) setQ(""); }, [user?.id]); // eslint-disable-line react-hooks/exhaustive-deps // Debounced search. Runs once on open (empty q → first page alphabetically) // and then 250ms after the operator stops typing. useEffect(() => { if (!user) return; let cancelled = false; setLoading(true); const t = setTimeout(async () => { try { const items = await api.disciples(q); if (!cancelled) setOptions(items); } catch (err) { if (!cancelled) { toast.error( err instanceof AdminError ? err.message : "Could not load disciples.", ); setOptions([]); } } finally { if (!cancelled) setLoading(false); } }, 250); return () => { cancelled = true; clearTimeout(t); }; }, [q, user?.id]); // eslint-disable-line react-hooks/exhaustive-deps const link = useCallback( async (d: AdminDisciple) => { if (!user) return; setBusyId(d.id); try { await api.linkDisciple(user.id, d.id); toast.success( `@${user.handle} linked to ${d.initiated_name} ${d.suffix}`.trim(), ); onSaved(); onClose(); } catch (err) { toast.error( err instanceof AdminError ? err.message : "Could not link disciple.", ); } finally { setBusyId(null); } }, [user, onSaved, onClose], ); async function unlink() { if (!user) return; setBusyId("__unlink__"); try { await api.unlinkDisciple(user.id); toast.success(`Removed the disciple link from @${user.handle}`); onSaved(); onClose(); } catch (err) { toast.error( err instanceof AdminError ? err.message : "Could not remove the link.", ); } finally { setBusyId(null); } } return (

Link this account to one of Śrīla Prabhupāda’s initiated disciples. Linked accounts show a badge on their profile and are highlighted in the disciples directory.

{user?.disciple_id ? (
Currently linked to
{user.disciple_name ?? ( unknown disciple ({user.disciple_id.slice(0, 8)}…) )}
) : null}
setQ(e.target.value)} placeholder="Search initiated names…" className="pl-9" autoFocus />
{loading && options.length === 0 ? (

Searching…

) : options.length === 0 ? (

No disciples match that search.

) : (
    {options.map((d) => { const isCurrent = d.id === user?.disciple_id; // Claimed by somebody else — not linkable until that link goes. const takenByOther = !!d.linked_handle && !isCurrent; const sub = placeYear(d); return (
  • ); })}
)}
); }