Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 2x 2x 46x 46x 46x 46x 46x 46x 46x 21x 8x 8x 12x 12x 12x 12x 12x 12x 12x 12x 10x 10x 10x 10x 10x 2x 1x 1x 12x 46x 13x | "use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Input, ErrorMessage } from "@/components/ui";
import { apiPost } from "@/lib/api-client";
import { z } from "zod";
const inviteSchema = z.object({
email: z.string().email("Invalid email address"),
});
interface InviteFormProps {
collectionId: string;
onSuccess?: () => void;
}
export function InviteForm({ collectionId, onSuccess }: InviteFormProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [emailNotSent, setEmailNotSent] = useState(false);
const [email, setEmail] = useState("");
// Auto-reset the success view after 3s, cleaning the timer up on unmount so we
// never call setSuccess on an unmounted component (purses-th14).
useEffect(() => {
if (!success) return;
const t = setTimeout(() => setSuccess(false), 3000);
return () => clearTimeout(t);
}, [success]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setSuccess(false);
setEmailNotSent(false);
setLoading(true);
try {
const validated = inviteSchema.parse({ email });
const result = await apiPost<{ emailSent?: boolean }>(
`/api/collections/${collectionId}/invitations`,
validated,
);
setSuccess(true);
setEmailNotSent(result.emailSent === false);
setEmail("");
// The members page is a server component; refresh so the new pending
// row (with its Copy link button) appears while this notice is visible.
router.refresh();
onSuccess?.();
} catch (err: any) {
if (err instanceof z.ZodError) {
setError(err.issues[0].message);
} else {
setError(err?.message || "Failed to send invitation");
}
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
{error && <ErrorMessage message={error} />}
{/* Persistent live region: pre-existing in the DOM so the text change on
success is announced reliably (a region mounted together with its text
is frequently skipped by screen readers). Both the success message and
the (rarer) email-not-sent notice share this one region.
`emailNotSent` is intentionally NOT gated on `success` here: the plain
success message auto-resets after 3s, but the email-not-sent notice
must stay visible — there's still a Copy link the user needs — until
the next submit begins (cleared there, same beat as `error`). */}
<div role="status" aria-live="polite">
{success && (
<div className="bg-success-soft border border-success-soft rounded-control px-4 py-3 text-success-deep">
Invitation sent successfully!
</div>
)}
{emailNotSent && (
<div className="mt-2 bg-info-soft border border-info-soft rounded-control px-4 py-3 text-info-deep">
The invite is saved — the email didn't go out, so share the link yourself with Copy
link.
</div>
)}
</div>
<Input
label="Invite by email"
type="email"
required
placeholder="curator@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
<Button type="submit" disabled={loading}>
{loading ? "Sending..." : "Send invite"}
</Button>
</form>
);
}
|