"use client";
import { useState, useEffect } from "react";
import { useOrganizationList, useUser, useAuth } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useRouter } from "next/navigation";
import { Sparkles, ArrowRight, ArrowLeft, Check, Mail } from "lucide-react";
import Image from "next/image";
import { completeOnboarding } from './_actions'
export default function Onboarding() {
const {
userMemberships,
userInvitations,
createOrganization,
setActive
} = useOrganizationList({
userMemberships: {
infinite: true,
keepPreviousData: true,
},
userInvitations: {
infinite: true,
keepPreviousData: true,
}
});
const { user, isLoaded: userLoaded } = useUser();
const { isSignedIn } = useAuth();
const router = useRouter();
const [step, setStep] = useState(1);
const [orgName, setOrgName] = useState("");
const [appName, setAppName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [shouldRedirect, setShouldRedirect] = useState(false);
const [acceptingInvitationId, setAcceptingInvitationId] = useState<string | null>(null);
useEffect(() => {
if (!isSignedIn) {
router.push("/");
}
if (shouldRedirect) {
const isAdmin = user?.organizationMemberships.some(
m => m.organization.id === userMemberships?.data?.[0]?.organization.id &&
m.role === "org:admin"
);
user?.reload()
router.push("/admin");
}
}, [isSignedIn, shouldRedirect, router]);
const handleCreateOrganization = async () => {
if (!orgName.trim()) {
setError("Organization name is required");
return;
}
setLoading(true);
setError("");
try {
if (createOrganization) {
const newOrg = await createOrganization({
name: orgName,
// Optionally set the creating user as admin
});
// Set the new organization as active
await setActive?.({ organization: newOrg.id });
// Configure the organization (add logo, etc)
await configureNewOrganization(newOrg.id);
setShouldRedirect(true);
}
} catch (err) {
console.error("Failed to create organization", err);
setError("Failed to create organization. Please try again.");
} finally {
setLoading(false);
}
};
const configureNewOrganization = async (orgId: string) => {
try {
// Here you would make API calls to configure the new organization
// For example, upload a default logo, set up initial settings, etc.
// await fetch('/api/configure-org', { method: 'POST', body: JSON.stringify({ orgId }) });
} catch (err) {
console.error("Failed to configure organization", err);
}
};
const handleJoinOrganization = async (orgId: string) => {
setLoading(true);
try {
if (setActive) {
await setActive({ organization: orgId });
const res = await completeOnboarding();
if (res?.success) {
user?.reload()
}
setShouldRedirect(true);
}
} catch (err) {
console.error("Failed to join organization", err);
setError("Failed to join organization. Please try again.");
} finally {
setLoading(false);
}
};
const configureUserAfterJoining = async (orgId: string) => {
try {
// Here you would make API calls to configure the user in the organization
// For example, set default permissions, add to groups, etc.
// await fetch('/api/configure-user', { method: 'POST', body: JSON.stringify({ orgId, userId: user?.id }) });
} catch (err) {
console.error("Failed to configure user", err);
}
};
const handleAcceptInvitation = async (invitationId: string) => {
setAcceptingInvitationId(invitationId);
try {
const invitation = userInvitations.data?.find(inv => inv.id === invitationId);
if (invitation) {
await invitation.accept();
const res = await completeOnboarding();
if (res?.success) {
// Reloads the user's data from the Clerk API
await user?.reload()
userInvitations.revalidate?.();
userMemberships.revalidate?.();
router.push("/admin" );
}
}
} catch (err) {
console.error("Failed to accept invitation", err);
setError("Failed to accept invitation. Please try again.");
} finally {
setAcceptingInvitationId(null);
}
};
if (!isSignedIn || !userLoaded) {
return (
<div className="flex justify-center items-center min-h-screen">
<p>Redirecting...</p>
</div>
);
}
const hasMemberships = userMemberships?.data && userMemberships.data.length > 0;
const hasInvitations = userInvitations?.data && userInvitations.data.length > 0;
return (
<div className="flex justify-center items-center min-h-screen bg-gradient-to-b from-violet-50 via-white to-sky-50">
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-200 rounded-full blur-3xl opacity-20" />
<div className="absolute top-20 -left-40 w-80 h-80 bg-blue-200 rounded-full blur-3xl opacity-20" />
<div className="absolute bottom-0 right-0 w-80 h-80 bg-pink-200 rounded-full blur-3xl opacity-20" />
</div>
<Card className="w-full max-w-md border-0 shadow-xl bg-white/80 backdrop-blur-sm">
<CardHeader>
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-purple-500" />
<CardTitle className="text-2xl font-bold text-gray-800">
{step === 1 && "Get Started"}
{step === 2 && "Create Organization"}
{step === 3 && "Join Organization"}
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<div className="text-red-500 text-sm p-3 bg-red-50 rounded-lg">
{error}
</div>
)}
{step === 1 && (
<div className="space-y-4">
<p className="text-gray-600">
Welcome, {user?.firstName}! Let's get you set up with an organization.
</p>
<Button
className="w-full rounded-full text-white h-12 bg-purple-600 hover:bg-purple-700"
onClick={() => setStep(2)}
>
Create a new organization <ArrowRight className="ml-2 h-4 w-4" />
</Button>
<Button
className="w-full rounded-full h-12"
variant="outline"
onClick={() => setStep(3)}
>
Join an existing organization <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Organization Name
</label>
<Input
placeholder="My Awesome Org"
value={orgName}
onChange={(e) => setOrgName(e.target.value)}
className="h-12 rounded-xl"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Application Name (optional)
</label>
<Input
placeholder="My Display App"
value={appName}
onChange={(e) => setAppName(e.target.value)}
className="h-12 rounded-xl"
/>
</div>
<div className="flex gap-3 pt-2">
<Button
className="rounded-full h-12 flex-1 bg-purple-600 hover:bg-purple-700"
onClick={handleCreateOrganization}
disabled={loading}
>
{loading ? "Creating..." : "Create Organization"}
</Button>
<Button
className="rounded-full h-12"
variant="outline"
onClick={() => setStep(1)}
disabled={loading}
>
<ArrowLeft className="h-4 w-4" />
</Button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-4">
{hasInvitations && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">Pending Invitations</h3>
{userInvitations.data?.map((invitation) => (
<div
key={invitation.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
{invitation.publicOrganizationData?.imageUrl ? (
<div className="relative h-10 w-10 rounded-md overflow-hidden">
<Image
src={invitation.publicOrganizationData.imageUrl}
alt={invitation.publicOrganizationData.name || 'Organization logo'}
fill
className="object-cover"
/>
</div>
) : (
<div className="h-10 w-10 rounded-md bg-purple-100 flex items-center justify-center">
<Mail className="h-5 w-5 text-purple-500" />
</div>
)}
<div>
<p className="font-medium">{invitation.publicOrganizationData?.name || 'Unknown Organization'}</p>
<p className="text-sm text-gray-500">
{invitation.emailAddress} - {invitation.status}
</p>
</div>
</div>
<Button
size="sm"
onClick={() => invitation.id && handleAcceptInvitation(invitation.id)}
disabled={loading || acceptingInvitationId === invitation.id}
className="hover:bg-purple-700 hover:text-white transition-colors"
>
{acceptingInvitationId === invitation.id ? "Accepting..." : "Accept"}
</Button>
</div>
))}
</div>
)}
{hasMemberships ? (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">Your Organizations</h3>
{userMemberships.data?.map((membership) => (
<Button
key={membership.id}
className="w-full justify-between h-14 rounded-xl hover:bg-gray-50"
variant="outline"
onClick={() => handleJoinOrganization(membership.organization.id)}
disabled={loading}
>
<div className="flex items-center gap-3">
{membership.organization.imageUrl ? (
<div className="relative h-8 w-8 rounded-md overflow-hidden">
<Image
src={membership.organization.imageUrl}
alt={membership.organization.name}
fill
className="object-cover"
/>
</div>
) : (
<div className="h-8 w-8 rounded-md bg-purple-100 flex items-center justify-center">
<Mail className="h-4 w-4 text-purple-500" />
</div>
)}
<span>{membership.organization.name}</span>
</div>
{user?.organizationMemberships?.[0]?.organization.id === membership.organization.id && (
<Check className="h-4 w-4 text-green-500" />
)}
</Button>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
{hasInvitations ? null : (
<>
<p>No organizations available to join.</p>
<p className="mt-2">Ask an admin to invite you to their organization.</p>
</>
)}
</div>
)}
<Button
className="w-full rounded-full h-12 mt-4 hover:bg-gray-50"
variant="outline"
onClick={() => setStep(1)}
disabled={loading}
>
<ArrowLeft className="h-4 w-4 mr-2" /> Back
</Button>
</div>
)}
</CardContent>
</Card>
</div>
);
}