From 3d3eb9672157394fbd246cb42124ec6782233c58 Mon Sep 17 00:00:00 2001 From: Marc Klose Date: Mon, 6 Apr 2026 02:59:11 +0300 Subject: [PATCH] Fix invitation bug and enhance UX with inline error messaging --- app/dashboard/settings/team/actions.ts | 104 +++++++++++++++++-------- app/dashboard/settings/team/page.tsx | 19 +---- components/invite-member-form.tsx | 47 +++++++++++ 3 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 components/invite-member-form.tsx diff --git a/app/dashboard/settings/team/actions.ts b/app/dashboard/settings/team/actions.ts index fde7362..5cd1e0f 100644 --- a/app/dashboard/settings/team/actions.ts +++ b/app/dashboard/settings/team/actions.ts @@ -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 { 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) { diff --git a/app/dashboard/settings/team/page.tsx b/app/dashboard/settings/team/page.tsx index 92ca70f..850447b 100644 --- a/app/dashboard/settings/team/page.tsx +++ b/app/dashboard/settings/team/page.tsx @@ -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() {

Invite New Member

-
- - -
+
)} diff --git a/components/invite-member-form.tsx b/components/invite-member-form.tsx new file mode 100644 index 0000000..6a0ec0d --- /dev/null +++ b/components/invite-member-form.tsx @@ -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(null); + const [state, formAction, isPending] = useActionState( + inviteUser, + null + ); + + useEffect(() => { + if (state?.success) { + formRef.current?.reset(); + } + }, [state]); + + return ( +
+
+ + +
+ + {state?.error && ( +

{state.error}

+ )} + {state?.success && ( +

{state.message}

+ )} +
+ ); +}