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) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
135 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|