"use client";
import * as React from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Ban, Check, Plus, ShieldCheck, Trash2, Undo2, X } from "lucide-react";
import { api, bytes, type AdminUser } from "@/lib/api";
import { useToast } from "@/components/toast";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function UsersTable({
users,
roles,
me,
refresh,
}: {
users: AdminUser[];
roles: string[];
me: string | null;
refresh: () => void;
}) {
const [adding, setAdding] = React.useState(false);
return (
Users
Adding someone here is the invitation — no email, no token. They sign in
with that provider and they’re in.
{adding && (
{
setAdding(false);
refresh();
}}
/>
)}
);
}
function UserRow({
u,
roles,
isMe,
refresh,
}: {
u: AdminUser;
roles: string[];
isMe: boolean;
refresh: () => void;
}) {
const toast = useToast();
const [busy, setBusy] = React.useState(false);
const [confirmDel, setConfirmDel] = React.useState(false);
const run = async (fn: () => Promise, ok: string) => {
setBusy(true);
try {
await fn();
toast(ok);
refresh();
} catch (e) {
toast((e as Error).message, true);
}
setBusy(false);
};
const quotaPct = u.quota ? (u.usage.requests / u.quota) * 100 : null;
return (
{u.avatar && (
/* eslint-disable-next-line @next/next/no-img-element */

(e.currentTarget.style.visibility = "hidden")}
/>
)}
{u.display || u.username}
{u.role === "admin" && }
{isMe && (you)}
{u.username} · {u.provider}
{u.last_seen && ` · seen ${u.last_seen.slice(0, 10)}`}
{u.disabled && " · disabled"}
{confirmDel ? (
) : (
)}
run(() => api.admin.patchUser(u.username, { quota: v }), "Quota updated")}
/>
run(() => api.admin.patchUser(u.username, { storage_quota_gb: v }), "Quota updated")
}
/>
{u.bootstrap && (
bootstrap account — disabling won’t lock them out
)}
);
}
/** Number input that only commits on blur or Enter, so a quota isn't PATCHed
* once per keystroke while you type "120". */
function QuotaField({
label,
value,
suffix,
pct,
onSave,
}: {
label: string;
value: number;
suffix: string;
pct: number | null;
onSave: (v: number) => void;
}) {
const [v, setV] = React.useState(String(value));
React.useEffect(() => setV(String(value)), [value]);
const commit = () => {
const n = Number(v);
if (!Number.isFinite(n) || n < 0) {
setV(String(value));
return;
}
if (n !== value) onSave(n);
};
return (
);
}
function InviteForm({ roles, onDone }: { roles: string[]; onDone: () => void }) {
const toast = useToast();
const [username, setUsername] = React.useState("");
const [provider, setProvider] = React.useState("github");
const [role, setRole] = React.useState("user");
const [quota, setQuota] = React.useState("0");
const [storage, setStorage] = React.useState("0");
const [busy, setBusy] = React.useState(false);
const submit = async () => {
if (!username.trim()) {
toast("Username required", true);
return;
}
setBusy(true);
try {
await api.admin.addUser({
username: username.trim(),
provider,
role,
quota: Number(quota) || 0,
storage_quota_gb: Number(storage) || 0,
});
toast(`Invited ${username.trim()}`);
onDone();
} catch (e) {
toast((e as Error).message, true);
}
setBusy(false);
};
const input =
"rounded-glass border border-edge-hi bg-transparent px-3 py-2 text-callout " +
"text-ink outline-none focus:border-accent-line";
return (
0 means unlimited.
);
}