Files
Nexus/apps/web/src/components/admin/InviteUserModal.tsx
T
Hartmut b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
2026-05-21 16:28:40 +02:00

135 lines
4.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { SystemRole } from "@nexus/shared";
import { trpc } from "~/lib/trpc/client.js";
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
interface InviteUserModalProps {
open: boolean;
onClose: () => void;
}
const ROLE_OPTIONS: { value: SystemRole; label: string }[] = [
{ value: SystemRole.USER, label: "User" },
{ value: SystemRole.VIEWER, label: "Viewer" },
{ value: SystemRole.CONTROLLER, label: "Controller" },
{ value: SystemRole.MANAGER, label: "Manager" },
{ value: SystemRole.ADMIN, label: "Admin" },
];
export function InviteUserModal({ open, onClose }: InviteUserModalProps) {
const [email, setEmail] = useState("");
const [role, setRole] = useState<SystemRole>(SystemRole.USER);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const utils = trpc.useUtils();
const inviteMutation = trpc.invite.createInvite.useMutation({
onSuccess: async () => {
await utils.invite.listInvites.invalidate();
setSuccess(true);
setEmail("");
setRole(SystemRole.USER);
setTimeout(() => {
setSuccess(false);
onClose();
}, 1500);
},
onError: (err) => setError(err.message),
});
function handleClose() {
setEmail("");
setRole(SystemRole.USER);
setError(null);
setSuccess(false);
onClose();
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!email) {
setError("Email is required.");
return;
}
await inviteMutation.mutateAsync({ email, role });
}
return (
<AnimatedModal open={open} onClose={handleClose} maxWidth="max-w-md">
<div className="px-6 py-5">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Invite User</h2>
{success ? (
<div className="rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 px-4 py-3 text-sm text-green-700 dark:text-green-400">
Invitation sent successfully.
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-4 py-3 text-sm text-red-700 dark:text-red-400">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email address
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
required
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Role
</label>
<select
value={role}
onChange={(e) => setRole(e.target.value as SystemRole)}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
>
{ROLE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
An invite link valid for 72 hours will be sent to this email address.
</p>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={handleClose}
className="rounded-lg border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={inviteMutation.isPending}
className="rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 transition-colors disabled:opacity-50"
>
{inviteMutation.isPending ? "Sending..." : "Send Invite"}
</button>
</div>
</form>
)}
</div>
</AnimatedModal>
);
}