Fix invitation bug and enhance UX with inline error messaging
This commit is contained in:
parent
521deea5ce
commit
3d3eb96721
3 changed files with 121 additions and 49 deletions
|
|
@ -3,45 +3,83 @@
|
|||
import { createClient } from "@/lib/supabase/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function inviteUser(formData: FormData) {
|
||||
export type InviteStatus = {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export async function inviteUser(prevState: InviteStatus | null, formData: FormData): Promise<InviteStatus> {
|
||||
const email = formData.get("email") as string;
|
||||
if (!email) {
|
||||
throw new Error("Email is required");
|
||||
return { error: "Email is required" };
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const { data: userData, error: userError } = await supabase.auth.getUser();
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: userData, error: userError } = await supabase.auth.getUser();
|
||||
|
||||
if (userError || !userData.user) {
|
||||
throw new Error("Unauthorized");
|
||||
if (userError || !userData.user) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("organization_id, role")
|
||||
.eq("id", userData.user.id)
|
||||
.single();
|
||||
|
||||
if (!profile || profile.role !== "owner" || !profile.organization_id) {
|
||||
return { error: "Only organization owners can invite new members" };
|
||||
}
|
||||
|
||||
// Check if user is already a member
|
||||
const { data: existingMember } = await supabase
|
||||
.from("profiles")
|
||||
.select("id")
|
||||
.eq("email", email)
|
||||
.eq("organization_id", profile.organization_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingMember) {
|
||||
return { error: "User is already a member of this organization" };
|
||||
}
|
||||
|
||||
// Check if there's already a pending invitation
|
||||
const { data: existingInvitation } = await supabase
|
||||
.from("invitations")
|
||||
.select("id")
|
||||
.eq("email", email)
|
||||
.eq("organization_id", profile.organization_id)
|
||||
.eq("status", "pending")
|
||||
.gt("expires_at", new Date().toISOString())
|
||||
.maybeSingle();
|
||||
|
||||
if (existingInvitation) {
|
||||
return { error: "An invitation is already pending for this email address" };
|
||||
}
|
||||
|
||||
const { error: inviteError } = await supabase
|
||||
.from("invitations")
|
||||
.insert([
|
||||
{
|
||||
email,
|
||||
organization_id: profile.organization_id,
|
||||
invited_by: userData.user.id,
|
||||
},
|
||||
]);
|
||||
|
||||
if (inviteError) {
|
||||
console.error("Error creating invitation:", inviteError);
|
||||
return { error: "Failed to create invitation" };
|
||||
}
|
||||
|
||||
revalidatePath("/dashboard/settings/team");
|
||||
return { success: true, message: "Invitation sent successfully" };
|
||||
} catch (error) {
|
||||
console.error("Unexpected error inviting user:", error);
|
||||
return { error: "An unexpected error occurred" };
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("organization_id, role")
|
||||
.eq("id", userData.user.id)
|
||||
.single();
|
||||
|
||||
if (!profile || profile.role !== "owner" || !profile.organization_id) {
|
||||
throw new Error("Only organization owners can invite new members");
|
||||
}
|
||||
|
||||
const { error: inviteError } = await supabase
|
||||
.from("invitations")
|
||||
.insert([
|
||||
{
|
||||
email,
|
||||
organization_id: profile.organization_id,
|
||||
invited_by: userData.user.id,
|
||||
},
|
||||
]);
|
||||
|
||||
if (inviteError) {
|
||||
console.error("Error creating invitation:", inviteError);
|
||||
throw new Error("Failed to create invitation");
|
||||
}
|
||||
|
||||
revalidatePath("/dashboard/settings/team");
|
||||
}
|
||||
|
||||
export async function revokeInvitation(formData: FormData) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { createClient } from "@/lib/supabase/server";
|
||||
import { inviteUser, revokeInvitation } from "./actions";
|
||||
import { revokeInvitation } from "./actions";
|
||||
import { Suspense } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { InviteMemberForm } from "@/components/invite-member-form";
|
||||
|
||||
async function TeamSettingsContent() {
|
||||
const supabase = await createClient();
|
||||
|
|
@ -61,21 +62,7 @@ async function TeamSettingsContent() {
|
|||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-medium text-foreground border-b border-border pb-2">Invite New Member</h2>
|
||||
<div className="bg-card border border-border rounded-lg p-4 shadow-sm">
|
||||
<form action={inviteUser} className="flex gap-4">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email address"
|
||||
required
|
||||
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-primary text-primary-foreground px-4 py-2 rounded-md text-sm font-medium hover:bg-primary/90 transition-colors flex-shrink-0"
|
||||
>
|
||||
Send Invite
|
||||
</button>
|
||||
</form>
|
||||
<InviteMemberForm />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
47
components/invite-member-form.tsx
Normal file
47
components/invite-member-form.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useRef } from "react";
|
||||
import { inviteUser, type InviteStatus } from "@/app/dashboard/settings/team/actions";
|
||||
|
||||
export function InviteMemberForm() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [state, formAction, isPending] = useActionState<InviteStatus | null, FormData>(
|
||||
inviteUser,
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<form ref={formRef} action={formAction} className="flex gap-4">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email address"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="bg-primary text-primary-foreground px-4 py-2 rounded-md text-sm font-medium hover:bg-primary/90 transition-colors flex-shrink-0 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? "Sending..." : "Send Invite"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{state?.error && (
|
||||
<p className="text-sm text-destructive mt-1 animate-in fade-in slide-in-from-top-1">{state.error}</p>
|
||||
)}
|
||||
{state?.success && (
|
||||
<p className="text-sm text-green-500 mt-1 animate-in fade-in slide-in-from-top-1">{state.message}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue