Files
pieced-portal/src/components/team/invite-form.tsx
admin a31d05b7c2
All checks were successful
Build and Push / build (push) Successful in 1m26s
Team UI
2026-04-26 23:07:47 +02:00

151 lines
5.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
type FormState = "idle" | "submitting" | "success" | "error";
/**
* InviteForm — owner submits email + name + role to /api/team/invite.
* On success, broadcasts `team:refresh` so the sibling TeamList
* re-fetches the member list.
*
* Form fields mirror the POST body:
* { email, givenName, familyName, role: "owner" | "user" }
*
* Role defaults to "user" — the more conservative grant. Owner
* promotion happens in ZITADEL Console for now.
*/
export function InviteForm() {
const t = useTranslations("team");
const tCommon = useTranslations("common");
const [form, setForm] = useState({
email: "",
givenName: "",
familyName: "",
role: "user" as "owner" | "user",
});
const [state, setState] = useState<FormState>("idle");
const [error, setError] = useState("");
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setState("submitting");
try {
const res = await fetch("/api/team/invite", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const data = await res.json();
if (data.code === "user_already_exists") {
throw new Error(t("inviteUserExists"));
}
throw new Error(data.error || "Invite failed");
}
setState("success");
setForm({ email: "", givenName: "", familyName: "", role: "user" });
// Tell the TeamList sibling to refresh
window.dispatchEvent(new Event("team:refresh"));
// Auto-clear the success banner after a moment so the form
// doesn't permanently look "done"
setTimeout(() => setState("idle"), 3500);
} catch (err: any) {
setError(err.message);
setState("error");
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
{t("givenName")}
</label>
<input
name="givenName"
type="text"
required
value={form.givenName}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
/>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
{t("familyName")}
</label>
<input
name="familyName"
type="text"
required
value={form.familyName}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
{t("email")}
</label>
<input
name="email"
type="email"
required
value={form.email}
onChange={handleChange}
placeholder="colleague@company.ch"
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
/>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
{t("role")}
</label>
<select
name="role"
value={form.role}
onChange={handleChange}
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
>
<option value="user">{t("roleUser")}</option>
<option value="owner">{t("roleOwner")}</option>
</select>
<p className="text-xs text-text-muted mt-1">{t("roleHint")}</p>
</div>
{error && (
<div className="text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded-lg px-3 py-2">
{error}
</div>
)}
{state === "success" && (
<div className="text-xs text-emerald-400 bg-emerald-400/10 border border-emerald-400/20 rounded-lg px-3 py-2">
{t("inviteSent")}
</div>
)}
<button
type="submit"
disabled={state === "submitting"}
className="w-full py-2.5 px-4 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{state === "submitting" ? tCommon("loading") : t("inviteButton")}
</button>
</form>
);
}