"use client"; import { useCallback, useState } from "react"; import { Mail } from "lucide-react"; import { toast } from "sonner"; import { api, AdminError, type AdminUser } from "@/lib/api"; import { GoogleIcon, FacebookIcon } from "@/components/ui/provider-icons"; import { usePagedList } from "@/lib/use-paged-list"; import { ListShell, EmptyRow } from "@/components/list-shell"; import { Table, THead, TR, TH, TD } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Modal } from "@/components/ui/modal"; import { RowMenu, type MenuItem } from "@/components/ui/menu"; import { Avatar } from "@/components/ui/avatar"; import { useConfirm } from "@/components/confirm"; import { Input, Textarea } from "@/components/ui/input"; import { fmtDate } from "@/lib/utils"; export default function UsersPage() { const [q, setQ] = useState(""); const fetcher = useCallback( (limit: number, offset: number) => api.users(q, limit, offset), [q], ); const list = usePagedList(fetcher, [q]); const confirm = useConfirm(); const [notify, setNotify] = useState(null); async function toggleBan(u: AdminUser) { const banning = !u.banned_at; if ( banning && !(await confirm({ title: `Suspend @${u.handle}?`, message: "They'll be signed out and unable to log in until you unban them.", confirmLabel: "Suspend account", destructive: true, })) ) { return; } try { await (banning ? api.banUser(u.id) : api.unbanUser(u.id)); toast.success(banning ? "Account suspended." : "Account restored."); list.refresh(); } catch (err) { toast.error(err instanceof AdminError ? err.message : "Action failed."); } } function menuItems(u: AdminUser): MenuItem[] { const items: MenuItem[] = [ { label: "Send notification", onClick: () => setNotify(u) }, ]; // No ban option for fellow admins (avoid locking each other out). if (u.role !== "admin") { items.push({ label: u.banned_at ? "Unban account" : "Ban account", onClick: () => toggleBan(u), destructive: !u.banned_at, }); } return items; } return ( {list.items.length === 0 && !list.loading ? ( ) : ( list.items.map((u) => ( )) )}
User Email Posts Joined Actions
{u.name} {u.banned_at ? ( Suspended ) : null}
@{u.handle}
{u.email ?? ( — )}
{u.post_count} {fmtDate(u.created_at)}
setNotify(null)} />
); } function AuthIcons({ user }: { user: AdminUser }) { const hasGoogle = user.providers.includes("google"); const hasFacebook = user.providers.includes("facebook"); if (!user.has_password && !hasGoogle && !hasFacebook) { return —; } return (
{user.has_password ? ( // Tooltip shows the email on hover; clicking copies it to the clipboard. ) : null} {hasGoogle ? : null} {hasFacebook ? : null}
); } function NotifyModal({ user, onClose, }: { user: AdminUser | null; onClose: () => void; }) { const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const [busy, setBusy] = useState(false); async function send() { if (!user || body.trim() === "") return; setBusy(true); try { await api.notifyUser(user.id, title.trim(), body.trim()); toast.success(`Notification sent to @${user.handle}`); setTitle(""); setBody(""); onClose(); } catch (err) { toast.error(err instanceof AdminError ? err.message : "Could not send."); } finally { setBusy(false); } } return (

Sends an in-app notification, plus web push and email where available.